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, has_closer) = if single_column_here {
852 let end_pos = find_single_column_table_end(window, start_pos + 1)?;
853 (start_pos, end_pos, true)
854 } else {
855 let separator_pos = find_separator_line(window, start_pos)?;
856 let headerless = separator_pos == start_pos;
860 let (end_pos, has_closer) = find_table_end(window, separator_pos + 1, headerless)?;
861 (separator_pos, end_pos, has_closer)
862 };
863 log::trace!(" found separator at line {}", separator_pos + 1);
864
865 let separator_line = window.line(separator_pos);
866 let mut columns = if single_column_here {
867 parse_single_dash_run(separator_line)?
868 } else {
869 try_parse_table_separator(separator_line)?
870 };
871
872 let has_header = separator_pos > start_pos;
874 let header_line = if has_header {
875 Some(window.line(separator_pos - 1))
876 } else {
877 None
878 };
879
880 determine_alignments(&mut columns, separator_line, header_line);
882
883 let data_rows = end_pos - separator_pos - 1 - usize::from(has_closer);
887
888 if data_rows == 0 {
889 return None;
890 }
891
892 let caption_before = find_caption_before_table(window, start_pos);
894
895 let caption_after = if caption_before.is_some() {
897 None
898 } else {
899 find_caption_after_table(window, end_pos)
900 };
901
902 builder.start_node(SyntaxKind::SIMPLE_TABLE.into());
904
905 if let Some((cap_start, cap_end)) = caption_before {
907 emit_table_caption(builder, window, cap_start, cap_end, config);
908 emit_caption_blank_lines(builder, window, cap_end, start_pos);
910 }
911
912 if has_header {
916 emit_table_row(
917 builder,
918 window,
919 separator_pos - 1,
920 &columns,
921 SyntaxKind::TABLE_HEADER,
922 config,
923 );
924 }
925
926 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
929 let separator_tail = window.emit_or_dispatch_tail(builder, separator_pos);
930 emit_separator_tokens(builder, separator_tail);
931 builder.finish_node();
932
933 let rows_end = if has_closer { end_pos - 1 } else { end_pos };
936 for idx in (separator_pos + 1)..rows_end {
937 emit_table_row(
938 builder,
939 window,
940 idx,
941 &columns,
942 SyntaxKind::TABLE_ROW,
943 config,
944 );
945 }
946
947 if has_closer {
948 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
949 let closer_tail = window.emit_or_dispatch_tail(builder, end_pos - 1);
950 emit_separator_tokens(builder, closer_tail);
951 builder.finish_node();
952 }
953
954 if let Some((cap_start, cap_end)) = caption_after {
956 emit_caption_blank_lines(builder, window, end_pos, cap_start);
958 emit_table_caption(builder, window, cap_start, cap_end, config);
959 }
960
961 builder.finish_node(); let table_start = if let Some((cap_start, _)) = caption_before {
965 cap_start
966 } else if has_header {
967 separator_pos - 1
968 } else {
969 separator_pos
970 };
971
972 let table_end = if let Some((_, cap_end)) = caption_after {
973 cap_end
974 } else {
975 end_pos
976 };
977
978 let lines_consumed = table_end - table_start;
979
980 Some(lines_consumed)
981}
982
983fn find_separator_line(lines: &(impl LineView + ?Sized), start_pos: usize) -> Option<usize> {
985 log::trace!(" find_separator_line from line {}", start_pos + 1);
986
987 log::trace!(" checking first line: {:?}", lines.line(start_pos));
989 if try_parse_table_separator(lines.line(start_pos)).is_some() {
990 log::trace!(" separator found at first line");
991 return Some(start_pos);
992 }
993
994 if start_pos + 1 < lines.line_count()
996 && !lines.line(start_pos).trim().is_empty()
997 && try_parse_table_separator(lines.line(start_pos + 1)).is_some()
998 {
999 return Some(start_pos + 1);
1000 }
1001 None
1002}
1003
1004fn find_table_end(
1011 lines: &(impl LineView + ?Sized),
1012 start_pos: usize,
1013 require_closer: bool,
1014) -> Option<(usize, bool)> {
1015 let mut saw_row = false;
1016 for i in start_pos..lines.line_count() {
1017 if lines.line(i).trim().is_empty() {
1018 return (!require_closer).then_some((i, false));
1019 }
1020 if try_parse_table_separator(lines.line(i)).is_some()
1022 && (i + 1 >= lines.line_count() || lines.line(i + 1).trim().is_empty())
1023 {
1024 if require_closer && !saw_row {
1027 return None;
1028 }
1029 return Some((i + 1, true));
1030 }
1031 saw_row = true;
1032 }
1033 (!require_closer).then_some((lines.line_count(), false))
1034}
1035
1036fn emit_table_row(
1039 builder: &mut GreenNodeBuilder<'static>,
1040 window: &StrippedLines<'_, '_>,
1041 abs_idx: usize,
1042 columns: &[Column],
1043 row_kind: SyntaxKind,
1044 config: &ParserOptions,
1045) {
1046 builder.start_node(row_kind.into());
1047
1048 let line = window.emit_or_dispatch_tail(builder, abs_idx);
1053
1054 let (line_without_newline, newline_str) = strip_newline(line);
1055
1056 let trimmed = line_without_newline.trim_start();
1058 let leading_ws_len = line_without_newline.len() - line_without_newline.trim_start().len();
1059 if leading_ws_len > 0 {
1060 builder.token(
1061 SyntaxKind::WHITESPACE.into(),
1062 &line_without_newline[..leading_ws_len],
1063 );
1064 }
1065
1066 let mut current_pos = 0;
1068
1069 for (i, col) in columns.iter().enumerate() {
1071 let cell_start = if col.start >= leading_ws_len {
1073 column_offset_to_byte_index(trimmed, col.start - leading_ws_len)
1074 } else {
1075 0
1076 };
1077
1078 let end_offset = columns.get(i + 1).map_or(usize::MAX, |next| next.start);
1084 let cell_end = if end_offset == usize::MAX {
1085 trimmed.len()
1086 } else if end_offset >= leading_ws_len {
1087 column_offset_to_byte_index(trimmed, end_offset - leading_ws_len)
1088 } else {
1089 0
1090 };
1091
1092 let cell_text = if cell_start < cell_end && cell_start < trimmed.len() {
1097 &trimmed[cell_start..cell_end]
1098 } else {
1099 ""
1100 };
1101
1102 let cell_content = cell_text.trim();
1103 let cell_content_start = cell_text.len() - cell_text.trim_start().len();
1104
1105 let content_abs_pos = (cell_start + cell_content_start).min(trimmed.len());
1107 if current_pos < content_abs_pos {
1108 builder.token(
1109 SyntaxKind::WHITESPACE.into(),
1110 &trimmed[current_pos..content_abs_pos],
1111 );
1112 }
1113
1114 emit_table_cell(builder, cell_content, config);
1116
1117 current_pos = content_abs_pos + cell_content.len();
1119 }
1120
1121 if current_pos < trimmed.len() {
1123 builder.token(SyntaxKind::WHITESPACE.into(), &trimmed[current_pos..]);
1124 }
1125
1126 if !newline_str.is_empty() {
1128 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
1129 }
1130
1131 builder.finish_node();
1132}
1133
1134fn try_parse_pipe_separator(line: &str) -> Option<Vec<Alignment>> {
1141 let trimmed = line.trim();
1142
1143 if !trimmed.contains('|') && !trimmed.contains('+') {
1145 return None;
1146 }
1147
1148 let cells: Vec<&str> = if trimmed.contains('+') {
1150 trimmed.split(['|', '+']).collect()
1152 } else {
1153 trimmed.split('|').collect()
1154 };
1155
1156 let mut alignments = Vec::new();
1157
1158 for cell in cells {
1159 let cell = cell.trim();
1160
1161 if cell.is_empty() {
1163 continue;
1164 }
1165
1166 let starts_colon = cell.starts_with(':');
1168 let ends_colon = cell.ends_with(':');
1169
1170 let without_colons = cell.trim_start_matches(':').trim_end_matches(':');
1172
1173 if without_colons.is_empty() || !without_colons.chars().all(|c| c == '-') {
1175 return None;
1176 }
1177
1178 let alignment = match (starts_colon, ends_colon) {
1180 (true, true) => Alignment::Center,
1181 (true, false) => Alignment::Left,
1182 (false, true) => Alignment::Right,
1183 (false, false) => Alignment::Default,
1184 };
1185
1186 alignments.push(alignment);
1187 }
1188
1189 if alignments.is_empty() {
1191 None
1192 } else {
1193 Some(alignments)
1194 }
1195}
1196
1197fn parse_pipe_table_row(line: &str) -> Vec<String> {
1200 let trimmed = line.trim();
1201
1202 let mut cells = Vec::new();
1203 let mut current_cell = String::new();
1204 let mut chars = trimmed.chars().peekable();
1205 let mut char_count = 0;
1206
1207 while let Some(ch) = chars.next() {
1208 char_count += 1;
1209 match ch {
1210 '\\' => {
1211 if let Some(&'|') = chars.peek() {
1213 current_cell.push('\\');
1214 current_cell.push('|');
1215 chars.next(); } else {
1217 current_cell.push(ch);
1218 }
1219 }
1220 '|' => {
1221 if char_count == 1 {
1223 continue; }
1225
1226 cells.push(current_cell.trim().to_string());
1228 current_cell.clear();
1229 }
1230 _ => {
1231 current_cell.push(ch);
1232 }
1233 }
1234 }
1235
1236 let trimmed_cell = current_cell.trim().to_string();
1238 if !trimmed_cell.is_empty() {
1239 cells.push(trimmed_cell);
1240 }
1241
1242 cells
1243}
1244
1245fn emit_pipe_table_row(
1248 builder: &mut GreenNodeBuilder<'static>,
1249 window: &StrippedLines<'_, '_>,
1250 abs_idx: usize,
1251 row_kind: SyntaxKind,
1252 config: &ParserOptions,
1253) {
1254 builder.start_node(row_kind.into());
1255
1256 let line = if abs_idx == window.dispatch_pos() {
1264 window.dispatch_tail()
1265 } else {
1266 window.emit_prefix_at(builder, abs_idx)
1267 };
1268
1269 let (line_without_newline, newline_str) = strip_newline(line);
1270 let trimmed = line_without_newline.trim();
1271
1272 let mut cell_starts = Vec::new();
1274 let mut cell_ends = Vec::new();
1275 let mut in_escape = false;
1276
1277 let mut pipe_positions = Vec::new();
1279 for (i, ch) in trimmed.char_indices() {
1280 if in_escape {
1281 in_escape = false;
1282 continue;
1283 }
1284 if ch == '\\' {
1285 in_escape = true;
1286 continue;
1287 }
1288 if ch == '|' {
1289 pipe_positions.push(i);
1290 }
1291 }
1292
1293 if pipe_positions.is_empty() {
1295 cell_starts.push(0);
1297 cell_ends.push(trimmed.len());
1298 } else {
1299 let start_pipe = pipe_positions.first() == Some(&0);
1301 let end_pipe = pipe_positions.last() == Some(&(trimmed.len() - 1));
1303
1304 if start_pipe {
1305 for i in 1..pipe_positions.len() {
1307 cell_starts.push(pipe_positions[i - 1] + 1);
1308 cell_ends.push(pipe_positions[i]);
1309 }
1310 if !end_pipe {
1312 cell_starts.push(*pipe_positions.last().unwrap() + 1);
1313 cell_ends.push(trimmed.len());
1314 }
1315 } else {
1316 cell_starts.push(0);
1318 cell_ends.push(pipe_positions[0]);
1319
1320 for i in 1..pipe_positions.len() {
1321 cell_starts.push(pipe_positions[i - 1] + 1);
1322 cell_ends.push(pipe_positions[i]);
1323 }
1324
1325 if !end_pipe {
1327 cell_starts.push(*pipe_positions.last().unwrap() + 1);
1328 cell_ends.push(trimmed.len());
1329 }
1330 }
1331 }
1332
1333 let leading_ws_len = line_without_newline.len() - line_without_newline.trim_start().len();
1335 if leading_ws_len > 0 {
1336 builder.token(
1337 SyntaxKind::WHITESPACE.into(),
1338 &line_without_newline[..leading_ws_len],
1339 );
1340 }
1341
1342 for (idx, (start, end)) in cell_starts.iter().zip(cell_ends.iter()).enumerate() {
1344 if *start > 0 {
1346 builder.token(SyntaxKind::TEXT.into(), "|");
1347 } else if idx == 0 && trimmed.starts_with('|') {
1348 builder.token(SyntaxKind::TEXT.into(), "|");
1350 }
1351
1352 let cell_with_ws = &trimmed[*start..*end];
1354 let cell_content = cell_with_ws.trim();
1355
1356 let cell_leading_ws = &cell_with_ws[..cell_with_ws.len() - cell_with_ws.trim_start().len()];
1358 if !cell_leading_ws.is_empty() {
1359 builder.token(SyntaxKind::WHITESPACE.into(), cell_leading_ws);
1360 }
1361
1362 emit_table_cell(builder, cell_content, config);
1364
1365 let cell_trailing_ws_start = cell_leading_ws.len() + cell_content.len();
1367 if cell_trailing_ws_start < cell_with_ws.len() {
1368 builder.token(
1369 SyntaxKind::WHITESPACE.into(),
1370 &cell_with_ws[cell_trailing_ws_start..],
1371 );
1372 }
1373 }
1374
1375 if !pipe_positions.is_empty() && trimmed.ends_with('|') {
1377 builder.token(SyntaxKind::TEXT.into(), "|");
1378 }
1379
1380 let trailing_ws_start = leading_ws_len + trimmed.len();
1382 if trailing_ws_start < line_without_newline.len() {
1383 builder.token(
1384 SyntaxKind::WHITESPACE.into(),
1385 &line_without_newline[trailing_ws_start..],
1386 );
1387 }
1388
1389 if !newline_str.is_empty() {
1391 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
1392 }
1393
1394 builder.finish_node();
1395}
1396
1397pub(crate) fn try_parse_pipe_table(
1400 window: &StrippedLines<'_, '_>,
1401 builder: &mut GreenNodeBuilder<'static>,
1402 config: &ParserOptions,
1403) -> Option<usize> {
1404 let lines = window.raw();
1405 let start_pos = window.pos();
1406 if start_pos + 1 >= lines.len() {
1407 return None;
1408 }
1409
1410 if !window.strip_at(start_pos).contains('|') && !is_caption_followed_by_table(window, start_pos)
1418 {
1419 return None;
1420 }
1421
1422 let (actual_start, caption_before) = if is_caption_followed_by_table(window, start_pos) {
1434 let (cap_start, cap_end) = caption_range_starting_at(window, start_pos)?;
1435 let mut pos = cap_end;
1436 while pos < window.line_count() && window.line(pos).trim().is_empty() {
1437 pos += 1;
1438 }
1439 (pos, Some((cap_start, cap_end)))
1440 } else {
1441 (start_pos, None)
1442 };
1443
1444 if actual_start + 1 >= lines.len() {
1445 return None;
1446 }
1447
1448 if !window.line(actual_start).contains('|') {
1450 return None;
1451 }
1452
1453 let alignments = try_parse_pipe_separator(window.line(actual_start + 1))?;
1455
1456 let header_cells = parse_pipe_table_row(window.line(actual_start));
1458
1459 if header_cells.len() != alignments.len() && !header_cells.is_empty() {
1461 if header_cells.len() < alignments.len() / 2 || header_cells.len() > alignments.len() * 2 {
1463 return None;
1464 }
1465 }
1466
1467 let mut end_pos = actual_start + 2;
1469 while end_pos < window.line_count() {
1470 let line = window.line(end_pos);
1471 if line.trim().is_empty() {
1472 break;
1473 }
1474 if !line.contains('|') {
1476 break;
1477 }
1478 end_pos += 1;
1479 }
1480
1481 if end_pos <= actual_start + 2 {
1483 return None;
1484 }
1485
1486 let caption_before = caption_before.or_else(|| find_caption_before_table(window, actual_start));
1488
1489 let caption_after = if caption_before.is_some() {
1491 None
1492 } else {
1493 find_caption_after_table(window, end_pos)
1494 };
1495
1496 builder.start_node(SyntaxKind::PIPE_TABLE.into());
1498
1499 if let Some((cap_start, cap_end)) = caption_before {
1501 emit_table_caption(builder, window, cap_start, cap_end, config);
1502 emit_caption_blank_lines(builder, window, cap_end, actual_start);
1504 }
1505
1506 emit_pipe_table_row(
1511 builder,
1512 window,
1513 actual_start,
1514 SyntaxKind::TABLE_HEADER,
1515 config,
1516 );
1517
1518 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
1521 let sep_idx = actual_start + 1;
1522 let separator_tail = if sep_idx == window.dispatch_pos() {
1523 window.dispatch_tail()
1524 } else {
1525 window.emit_prefix_at(builder, sep_idx)
1526 };
1527 emit_separator_tokens(builder, separator_tail);
1528 builder.finish_node();
1529
1530 for idx in (actual_start + 2)..end_pos {
1532 emit_pipe_table_row(builder, window, idx, SyntaxKind::TABLE_ROW, config);
1533 }
1534
1535 if let Some((cap_start, cap_end)) = caption_after {
1537 emit_caption_blank_lines(builder, window, end_pos, cap_start);
1539 emit_table_caption(builder, window, cap_start, cap_end, config);
1540 }
1541
1542 builder.finish_node(); let table_start = caption_before
1546 .map(|(start, _)| start)
1547 .unwrap_or(actual_start);
1548 let table_end = if let Some((_, cap_end)) = caption_after {
1549 cap_end
1550 } else {
1551 end_pos
1552 };
1553
1554 Some(table_end - table_start)
1555}
1556
1557#[cfg(test)]
1558mod tests {
1559 use super::super::container_prefix::ContainerPrefix;
1560 use super::*;
1561
1562 #[test]
1563 fn test_separator_detection() {
1564 assert!(try_parse_table_separator("------- ------ ---------- -------").is_some());
1565 assert!(try_parse_table_separator(" --- --- ---").is_some());
1566 assert!(try_parse_table_separator("-------").is_none()); assert!(try_parse_table_separator("--- --- ---").is_some()); }
1569
1570 #[test]
1571 fn test_column_extraction() {
1572 let line = "------- ------ ---------- -------";
1573 let columns = extract_columns(line, 0);
1574 assert_eq!(columns.len(), 4);
1575 }
1576
1577 #[test]
1578 fn column_offset_maps_by_display_width() {
1579 let line = "地號xy";
1583 assert_eq!(column_offset_to_byte_index(line, 4), 6);
1584 assert_eq!(column_offset_to_byte_index("abcd", 2), 2);
1586 }
1587
1588 #[test]
1589 fn simple_table_cjk_header_keeps_footnote_ref_intact() {
1590 let input = "\
1595 地號 地主 路段 總長[^d] 水利局舖的[^s]
1596------ ------- -------- --------- --------------
15972976 Ralph 南段 64 33
1598";
1599 let tree = crate::parser::parse(input, None);
1600 let refs: Vec<_> = tree
1601 .descendants()
1602 .filter(|n| n.kind() == SyntaxKind::FOOTNOTE_REFERENCE)
1603 .map(|n| n.text().to_string())
1604 .collect();
1605 assert_eq!(refs, vec!["[^d]".to_string(), "[^s]".to_string()]);
1606 }
1607
1608 #[test]
1609 fn test_simple_table_with_header() {
1610 let input = vec![
1611 " Right Left Center Default",
1612 "------- ------ ---------- -------",
1613 " 12 12 12 12",
1614 " 123 123 123 123",
1615 "",
1616 ];
1617
1618 let mut builder = GreenNodeBuilder::new();
1619 let prefix = ContainerPrefix::default();
1620 let window = StrippedLines::new(&input, 0, &prefix);
1621 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1622
1623 assert!(result.is_some());
1624 assert_eq!(result.unwrap(), 4); }
1626
1627 #[test]
1628 fn test_headerless_table() {
1629 let input = vec![
1630 "------- ------ ---------- -------",
1631 " 12 12 12 12",
1632 " 123 123 123 123",
1633 "------- ------ ---------- -------",
1634 "",
1635 ];
1636
1637 let mut builder = GreenNodeBuilder::new();
1638 let prefix = ContainerPrefix::default();
1639 let window = StrippedLines::new(&input, 0, &prefix);
1640 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1641
1642 assert!(result.is_some());
1643 assert_eq!(result.unwrap(), 4); }
1645
1646 fn assert_closer_is_separator(input: &str, expected_rows: usize) {
1649 let tree = crate::parser::parse(input, None);
1650 let table = tree
1651 .descendants()
1652 .find(|n| n.kind() == SyntaxKind::SIMPLE_TABLE)
1653 .expect("input should parse as a simple table");
1654 let separators = table
1655 .children()
1656 .filter(|n| n.kind() == SyntaxKind::TABLE_SEPARATOR)
1657 .count();
1658 assert_eq!(separators, 2, "opener and closer should both be separators");
1659 let rows: Vec<_> = table
1660 .children()
1661 .filter(|n| n.kind() == SyntaxKind::TABLE_ROW)
1662 .map(|n| n.text().to_string())
1663 .collect();
1664 assert_eq!(rows.len(), expected_rows, "rows: {rows:?}");
1665 for row in &rows {
1666 assert!(
1667 !row.trim().chars().all(|c| c == '-' || c.is_whitespace()),
1668 "all-dash closer emitted as TABLE_ROW: {row:?}"
1669 );
1670 }
1671 }
1672
1673 #[test]
1674 fn headerless_multi_column_closer_is_separator() {
1675 assert_closer_is_separator("--- ---\nfoo bar\n--- ---\n", 1);
1676 }
1677
1678 #[test]
1679 fn header_with_closer_is_separator() {
1680 assert_closer_is_separator("foo bar\n--- ---\n1 2\n--- ---\n", 1);
1681 }
1682
1683 #[test]
1684 fn headerless_single_column_closer_is_separator() {
1685 assert_closer_is_separator("----------\nfirst row\nsecond row\n----------\n", 2);
1686 }
1687
1688 #[test]
1689 fn headerless_multi_column_requires_closer() {
1690 let input = vec!["--- ---", "foo bar", "", "after"];
1694
1695 let mut builder = GreenNodeBuilder::new();
1696 let prefix = ContainerPrefix::default();
1697 let window = StrippedLines::new(&input, 0, &prefix);
1698 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1699
1700 assert!(result.is_none());
1701 }
1702
1703 #[test]
1704 fn headerless_multi_column_requires_closer_at_eof() {
1705 let input = vec!["--- ---", "foo bar"];
1707
1708 let mut builder = GreenNodeBuilder::new();
1709 let prefix = ContainerPrefix::default();
1710 let window = StrippedLines::new(&input, 0, &prefix);
1711 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1712
1713 assert!(result.is_none());
1714 }
1715
1716 #[test]
1717 fn header_with_closer_requires_rows() {
1718 let input = vec!["foo bar", "--- ---", "--- ---", ""];
1721
1722 let mut builder = GreenNodeBuilder::new();
1723 let prefix = ContainerPrefix::default();
1724 let window = StrippedLines::new(&input, 0, &prefix);
1725 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1726
1727 assert!(result.is_none());
1728 }
1729
1730 #[test]
1731 fn headerless_multi_column_requires_rows() {
1732 let input = vec!["--- ---", "--- ---", ""];
1735
1736 let mut builder = GreenNodeBuilder::new();
1737 let prefix = ContainerPrefix::default();
1738 let window = StrippedLines::new(&input, 0, &prefix);
1739 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1740
1741 assert!(result.is_none());
1742 }
1743
1744 #[test]
1745 fn headerless_multi_column_with_closer_at_eof() {
1746 let input = vec!["--- ---", "foo bar", "--- ---"];
1749
1750 let mut builder = GreenNodeBuilder::new();
1751 let prefix = ContainerPrefix::default();
1752 let window = StrippedLines::new(&input, 0, &prefix);
1753 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1754
1755 assert_eq!(result, Some(3)); }
1757
1758 #[test]
1759 fn single_dash_run_detection() {
1760 assert!(parse_single_dash_run("---").is_some());
1761 assert!(parse_single_dash_run("--").is_some()); assert!(parse_single_dash_run("-").is_none()); assert!(parse_single_dash_run(" ---------- ").is_some());
1764 assert!(parse_single_dash_run(" ---").is_none()); assert!(parse_single_dash_run("--- ---").is_none()); assert!(parse_single_dash_run("---x").is_none());
1767 assert!(parse_single_dash_run("- - -").is_none()); }
1769
1770 #[test]
1771 fn headerless_single_column_simple_table() {
1772 let input = vec!["---", "- a", "- b", "---", ""];
1777
1778 let mut builder = GreenNodeBuilder::new();
1779 let prefix = ContainerPrefix::default();
1780 let window = StrippedLines::new(&input, 0, &prefix);
1781 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1782
1783 assert_eq!(result, Some(4)); }
1785
1786 #[test]
1787 fn headerless_single_column_requires_closer() {
1788 let input = vec!["---", "just prose", "", "after"];
1791
1792 let mut builder = GreenNodeBuilder::new();
1793 let prefix = ContainerPrefix::default();
1794 let window = StrippedLines::new(&input, 0, &prefix);
1795 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1796
1797 assert!(result.is_none());
1798 }
1799
1800 #[test]
1801 fn headerless_single_column_requires_rows() {
1802 let input = vec!["---", "---", ""];
1804
1805 let mut builder = GreenNodeBuilder::new();
1806 let prefix = ContainerPrefix::default();
1807 let window = StrippedLines::new(&input, 0, &prefix);
1808 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1809
1810 assert!(result.is_none());
1811 }
1812
1813 #[test]
1814 fn test_caption_prefix_detection() {
1815 assert!(try_parse_caption_prefix("Table: My caption").is_some());
1816 assert!(try_parse_caption_prefix("table: My caption").is_some());
1817 assert!(try_parse_caption_prefix(": My caption").is_some());
1818 assert!(try_parse_caption_prefix(":").is_none()); assert!(try_parse_caption_prefix("Not a caption").is_none());
1820 }
1821
1822 #[test]
1823 fn table_grid_starts_at_matches_each_kind() {
1824 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));
1834
1835 assert!(!table_grid_starts_at(&["just some prose"][..], 0));
1837 assert!(!table_grid_starts_at(&["# Heading"][..], 0));
1838 assert!(!table_grid_starts_at(&["```", "code", "```"][..], 0)); assert!(!table_grid_starts_at(&["only one line"][..], 1)); }
1841
1842 #[test]
1847 fn caption_lookahead_agrees_with_real_parse() {
1848 let with_table = ": Cap\n\n| A | B |\n|---|---|\n| 1 | 2 |\n";
1849 let lines: Vec<&str> = with_table.lines().collect();
1850 assert!(is_caption_followed_by_table(&lines[..], 0));
1851 assert!(format!("{:#?}", crate::parse(with_table, None)).contains("PIPE_TABLE"));
1852
1853 let no_table = ": Cap\n\nplain paragraph\n";
1854 let lines: Vec<&str> = no_table.lines().collect();
1855 assert!(!is_caption_followed_by_table(&lines[..], 0));
1856 assert!(!format!("{:#?}", crate::parse(no_table, None)).contains("TABLE"));
1857 }
1858
1859 #[test]
1866 fn ordered_marker_on_pipe_table_line_is_top_level_table() {
1867 let input = "1. | a | b |\n | - | - |\n | 1 | 2 |\n";
1868 let tree = crate::parse(input, None);
1869 assert!(
1870 tree.descendants()
1871 .any(|n| n.kind() == SyntaxKind::PIPE_TABLE),
1872 "ordered marker + pipe table on the marker line should be a top-level table"
1873 );
1874 assert!(
1875 !tree.descendants().any(|n| n.kind() == SyntaxKind::LIST),
1876 "it must not nest under a list"
1877 );
1878 let dump = format!("{tree:#?}");
1880 assert!(
1881 dump.contains("1."),
1882 "marker text preserved as a header cell"
1883 );
1884 assert!(dump.contains('b'), "overflow cell `b` preserved (lossless)");
1885 }
1886
1887 #[test]
1888 fn lone_ordered_marker_pipe_line_is_a_list() {
1889 let input = "1. | a | b |\n";
1891 let tree = crate::parse(input, None);
1892 assert!(
1893 tree.descendants().any(|n| n.kind() == SyntaxKind::LIST),
1894 "a lone ordered marker line stays a list"
1895 );
1896 assert!(
1897 !tree
1898 .descendants()
1899 .any(|n| n.kind() == SyntaxKind::PIPE_TABLE),
1900 "no table without a delimiter row"
1901 );
1902 }
1903
1904 #[test]
1905 fn bullet_marker_on_pipe_table_line_stays_a_nested_table() {
1906 let input = "- | a | b |\n | - | - |\n | 1 | 2 |\n";
1908 let tree = crate::parse(input, None);
1909 assert!(
1910 tree.descendants().any(|n| n.kind() == SyntaxKind::LIST),
1911 "bullet marker keeps the list"
1912 );
1913 assert!(
1914 tree.descendants()
1915 .any(|n| n.kind() == SyntaxKind::PIPE_TABLE),
1916 "with the table nested inside the list item"
1917 );
1918 }
1919
1920 #[test]
1921 fn bare_colon_fenced_code_is_not_table_caption() {
1922 let input = "Term\n: ```\n code\n ```\n";
1923 let tree = crate::parse(input, None);
1924
1925 assert!(
1926 tree.descendants()
1927 .any(|node| node.kind() == SyntaxKind::DEFINITION_LIST),
1928 "should parse as definition list"
1929 );
1930 assert!(
1931 tree.descendants()
1932 .any(|node| node.kind() == SyntaxKind::CODE_BLOCK),
1933 "definition should preserve fenced code block"
1934 );
1935 assert!(
1936 !tree
1937 .descendants()
1938 .any(|node| node.kind() == SyntaxKind::TABLE_CAPTION),
1939 "fenced code definition should not be parsed as table caption"
1940 );
1941 }
1942
1943 #[test]
1944 fn bare_colon_caption_after_div_opening_is_table_caption() {
1945 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";
1946 let tree = crate::parse(input, None);
1947
1948 let caption_count = tree
1949 .descendants()
1950 .filter(|node| node.kind() == SyntaxKind::TABLE_CAPTION)
1951 .count();
1952 assert_eq!(
1953 caption_count, 2,
1954 "expected both captions to attach to tables"
1955 );
1956 assert!(
1957 !tree
1958 .descendants()
1959 .any(|node| node.kind() == SyntaxKind::DEFINITION_LIST),
1960 "caption lines in this fenced div table layout should not parse as definition list"
1961 );
1962 }
1963
1964 #[test]
1965 fn test_table_with_caption_after() {
1966 let input = vec![
1967 " Right Left Center Default",
1968 "------- ------ ---------- -------",
1969 " 12 12 12 12",
1970 " 123 123 123 123",
1971 "",
1972 "Table: Demonstration of simple table syntax.",
1973 "",
1974 ];
1975
1976 let mut builder = GreenNodeBuilder::new();
1977 let prefix = ContainerPrefix::default();
1978 let window = StrippedLines::new(&input, 0, &prefix);
1979 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
1980
1981 assert!(result.is_some());
1982 assert_eq!(result.unwrap(), 6);
1984 }
1985
1986 #[test]
1987 fn test_table_with_caption_before() {
1988 let input = vec![
1989 "Table: Demonstration of simple table syntax.",
1990 "",
1991 " Right Left Center Default",
1992 "------- ------ ---------- -------",
1993 " 12 12 12 12",
1994 " 123 123 123 123",
1995 "",
1996 ];
1997
1998 let mut builder = GreenNodeBuilder::new();
1999 let prefix = ContainerPrefix::default();
2000 let window = StrippedLines::new(&input, 2, &prefix);
2001 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
2002
2003 assert!(result.is_some());
2004 assert_eq!(result.unwrap(), 6);
2006 }
2007
2008 #[test]
2009 fn test_caption_with_colon_prefix() {
2010 let input = vec![
2011 " Right Left",
2012 "------- ------",
2013 " 12 12",
2014 "",
2015 ": Short caption",
2016 "",
2017 ];
2018
2019 let mut builder = GreenNodeBuilder::new();
2020 let prefix = ContainerPrefix::default();
2021 let window = StrippedLines::new(&input, 0, &prefix);
2022 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
2023
2024 assert!(result.is_some());
2025 assert_eq!(result.unwrap(), 5); }
2027
2028 #[test]
2029 fn test_multiline_caption() {
2030 let input = vec![
2031 " Right Left",
2032 "------- ------",
2033 " 12 12",
2034 "",
2035 "Table: This is a longer caption",
2036 "that spans multiple lines.",
2037 "",
2038 ];
2039
2040 let mut builder = GreenNodeBuilder::new();
2041 let prefix = ContainerPrefix::default();
2042 let window = StrippedLines::new(&input, 0, &prefix);
2043 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
2044
2045 assert!(result.is_some());
2046 assert_eq!(result.unwrap(), 6);
2048 }
2049
2050 #[test]
2051 fn test_simple_table_with_multibyte_cell_content() {
2052 let input = vec![
2053 "Name Hex code Hue C, M, Y, K (%) R, G, B (0-255) R, G, B (%)",
2054 "-------------- ------------ ------- ---------------- ----------------- ------------",
2055 " orange #E69F00 41° 0, 50, 100, 0 230, 159, 0 90, 60, 0",
2056 " sky blue #56B4E9 202° 80, 0, 0, 0 86, 180, 233 35, 70, 90",
2057 "",
2058 ];
2059
2060 let mut builder = GreenNodeBuilder::new();
2061 let prefix = ContainerPrefix::default();
2062 let window = StrippedLines::new(&input, 0, &prefix);
2063 let result = try_parse_simple_table(&window, &mut builder, &ParserOptions::default());
2064
2065 assert!(result.is_some());
2066 assert_eq!(result.unwrap(), 4);
2067 }
2068
2069 #[test]
2071 fn test_pipe_separator_detection() {
2072 assert!(try_parse_pipe_separator("|------:|:-----|---------|:------:|").is_some());
2073 assert!(try_parse_pipe_separator("|---|---|").is_some());
2074 assert!(try_parse_pipe_separator("-----|-----:").is_some()); assert!(try_parse_pipe_separator("|-----+-------|").is_some()); assert!(try_parse_pipe_separator("not a separator").is_none());
2077 }
2078
2079 #[test]
2080 fn test_pipe_alignments() {
2081 let aligns = try_parse_pipe_separator("|------:|:-----|---------|:------:|").unwrap();
2082 assert_eq!(aligns.len(), 4);
2083 assert_eq!(aligns[0], Alignment::Right);
2084 assert_eq!(aligns[1], Alignment::Left);
2085 assert_eq!(aligns[2], Alignment::Default);
2086 assert_eq!(aligns[3], Alignment::Center);
2087 }
2088
2089 #[test]
2090 fn test_parse_pipe_table_row() {
2091 let cells = parse_pipe_table_row("| Right | Left | Center |");
2092 assert_eq!(cells.len(), 3);
2093 assert_eq!(cells[0], "Right");
2094 assert_eq!(cells[1], "Left");
2095 assert_eq!(cells[2], "Center");
2096
2097 let cells2 = parse_pipe_table_row("Right | Left | Center");
2099 assert_eq!(cells2.len(), 3);
2100 }
2101
2102 #[test]
2103 fn test_basic_pipe_table() {
2104 let input = vec![
2105 "",
2106 "| Right | Left | Center |",
2107 "|------:|:-----|:------:|",
2108 "| 12 | 12 | 12 |",
2109 "| 123 | 123 | 123 |",
2110 "",
2111 ];
2112
2113 let mut builder = GreenNodeBuilder::new();
2114 let prefix = ContainerPrefix::default();
2115 let window = StrippedLines::new(&input, 1, &prefix);
2116 let result = try_parse_pipe_table(&window, &mut builder, &ParserOptions::default());
2117
2118 assert!(result.is_some());
2119 assert_eq!(result.unwrap(), 4); }
2121
2122 #[test]
2123 fn test_pipe_table_no_edge_pipes() {
2124 let input = vec![
2125 "",
2126 "fruit| price",
2127 "-----|-----:",
2128 "apple|2.05",
2129 "pear|1.37",
2130 "",
2131 ];
2132
2133 let mut builder = GreenNodeBuilder::new();
2134 let prefix = ContainerPrefix::default();
2135 let window = StrippedLines::new(&input, 1, &prefix);
2136 let result = try_parse_pipe_table(&window, &mut builder, &ParserOptions::default());
2137
2138 assert!(result.is_some());
2139 assert_eq!(result.unwrap(), 4);
2140 }
2141
2142 #[test]
2143 fn test_pipe_table_with_caption() {
2144 let input = vec![
2145 "",
2146 "| Col1 | Col2 |",
2147 "|------|------|",
2148 "| A | B |",
2149 "",
2150 "Table: My pipe table",
2151 "",
2152 ];
2153
2154 let mut builder = GreenNodeBuilder::new();
2155 let prefix = ContainerPrefix::default();
2156 let window = StrippedLines::new(&input, 1, &prefix);
2157 let result = try_parse_pipe_table(&window, &mut builder, &ParserOptions::default());
2158
2159 assert!(result.is_some());
2160 assert_eq!(result.unwrap(), 5); }
2162
2163 #[test]
2164 fn test_pipe_table_with_multiline_caption_before() {
2165 let input = vec![
2166 ": (#tab:base) base R quoting",
2167 "functions",
2168 "",
2169 "| C | D |",
2170 "|---|---|",
2171 "| 3 | 4 |",
2172 "",
2173 ];
2174
2175 let mut builder = GreenNodeBuilder::new();
2176 let prefix = ContainerPrefix::default();
2177 let window = StrippedLines::new(&input, 0, &prefix);
2178 let result = try_parse_pipe_table(&window, &mut builder, &ParserOptions::default());
2179
2180 assert!(result.is_some());
2181 assert_eq!(result.unwrap(), 6);
2183 }
2184}
2185
2186fn try_parse_grid_separator(line: &str) -> Option<Vec<GridColumn>> {
2193 let trimmed = line.trim_start();
2194 let leading_spaces = line.len() - trimmed.len();
2195
2196 if leading_spaces > 0 {
2201 return None;
2202 }
2203
2204 if !trimmed.starts_with('+') || !trimmed.trim_end().ends_with('+') {
2206 return None;
2207 }
2208
2209 let trimmed = trimmed.trim_end();
2211 let segments: Vec<&str> = trimmed.split('+').collect();
2212
2213 if segments.len() < 3 {
2215 return None;
2216 }
2217
2218 let mut columns = Vec::new();
2219
2220 for segment in segments.iter().skip(1).take(segments.len() - 2) {
2222 if segment.is_empty() {
2223 continue;
2224 }
2225
2226 let seg_trimmed = *segment;
2228
2229 let inner = seg_trimmed.trim_start_matches(':').trim_end_matches(':');
2231
2232 if inner.is_empty() {
2234 return None;
2235 }
2236
2237 let first_char = inner.chars().next().unwrap();
2238 if first_char != '-' && first_char != '=' {
2239 return None;
2240 }
2241
2242 if !inner.chars().all(|c| c == first_char) {
2243 return None;
2244 }
2245
2246 let is_header_sep = first_char == '=';
2247
2248 columns.push(GridColumn {
2249 is_header_separator: is_header_sep,
2250 width: seg_trimmed.chars().count(),
2251 });
2252 }
2253
2254 if columns.is_empty() {
2255 None
2256 } else {
2257 Some(columns)
2258 }
2259}
2260
2261#[derive(Debug, Clone)]
2263struct GridColumn {
2264 is_header_separator: bool,
2265 width: usize,
2266}
2267
2268fn slice_cell_by_display_width(line: &str, start_byte: usize, width: usize) -> (usize, usize) {
2269 let mut end_byte = start_byte;
2270 let mut display_cols = 0usize;
2271
2272 for (offset, ch) in line[start_byte..].char_indices() {
2273 if ch == '|' {
2274 let sep_byte = start_byte + offset;
2275 return (sep_byte, sep_byte + 1);
2276 }
2277 let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
2278 if display_cols + ch_width > width {
2279 break;
2280 }
2281 display_cols += ch_width;
2282 end_byte = start_byte + offset + ch.len_utf8();
2283 if display_cols >= width {
2284 break;
2285 }
2286 }
2287
2288 let mut sep_byte = end_byte;
2292 while sep_byte < line.len() {
2293 let mut chars = line[sep_byte..].chars();
2294 let Some(ch) = chars.next() else {
2295 break;
2296 };
2297 if ch == '|' {
2298 return (sep_byte, sep_byte + 1);
2299 }
2300 sep_byte += ch.len_utf8();
2301 }
2302
2303 (end_byte, end_byte)
2304}
2305
2306fn is_grid_content_row(line: &str) -> bool {
2309 let trimmed = line.trim_start();
2310 let leading_spaces = line.len() - trimmed.len();
2311
2312 if leading_spaces > 3 {
2313 return false;
2314 }
2315
2316 let trimmed = trimmed.trim_end();
2317 trimmed.starts_with('|') && (trimmed.ends_with('|') || trimmed.ends_with('+'))
2318}
2319
2320fn extract_grid_cells_from_line(line: &str, _columns: &[GridColumn]) -> Vec<String> {
2324 let (line_content, _) = strip_newline(line);
2325 let line_trimmed = line_content.trim();
2326
2327 if !line_trimmed.starts_with('|') || !line_trimmed.ends_with('|') {
2328 return vec![String::new(); _columns.len()];
2329 }
2330
2331 let mut cells = Vec::with_capacity(_columns.len());
2332 let mut pos_byte = 1; for col in _columns {
2335 let col_idx = cells.len();
2336 if pos_byte >= line_trimmed.len() {
2337 cells.push(String::new());
2338 continue;
2339 }
2340
2341 let start_byte = pos_byte;
2342 let end_byte = if col_idx + 1 == _columns.len() {
2343 line_trimmed.len().saturating_sub(1) } else {
2345 let (end, next_start) = slice_cell_by_display_width(line_trimmed, pos_byte, col.width);
2346 pos_byte = next_start;
2347 end
2348 };
2349 cells.push(line_trimmed[start_byte..end_byte].trim().to_string());
2350 if col_idx + 1 == _columns.len() {
2351 pos_byte = line_trimmed.len();
2352 }
2353 }
2354
2355 cells
2356}
2357
2358fn emit_grid_table_row(
2362 builder: &mut GreenNodeBuilder<'static>,
2363 window: &StrippedLines<'_, '_>,
2364 indices: &[usize],
2365 columns: &[GridColumn],
2366 row_kind: SyntaxKind,
2367 config: &ParserOptions,
2368) {
2369 if indices.is_empty() {
2370 return;
2371 }
2372
2373 builder.start_node(row_kind.into());
2374
2375 let first_line = window.emit_or_dispatch_tail(builder, indices[0]);
2381 let cell_contents = extract_grid_cells_from_line(first_line, columns);
2382 let (line_without_newline, newline_str) = strip_newline(first_line);
2383 let trimmed = line_without_newline.trim();
2384 let expected_pipe_count = columns.len().saturating_add(1);
2385 let actual_pipe_count = trimmed.chars().filter(|&c| c == '|').count();
2386
2387 if actual_pipe_count != expected_pipe_count {
2391 emit_line_tokens(builder, first_line);
2392 for &idx in &indices[1..] {
2393 let tail = window.emit_or_dispatch_tail(builder, idx);
2394 emit_line_tokens(builder, tail);
2395 }
2396 builder.finish_node();
2397 return;
2398 }
2399
2400 let leading_ws_len = line_without_newline.len() - line_without_newline.trim_start().len();
2402 if leading_ws_len > 0 {
2403 builder.token(
2404 SyntaxKind::WHITESPACE.into(),
2405 &line_without_newline[..leading_ws_len],
2406 );
2407 }
2408
2409 if trimmed.starts_with('|') {
2411 builder.token(SyntaxKind::TEXT.into(), "|");
2412 }
2413
2414 let mut pos_byte = 1usize; for (idx, cell_content) in cell_contents.iter().enumerate() {
2417 let part = if idx < columns.len() && pos_byte <= trimmed.len() {
2418 let start_byte = pos_byte;
2419 let end_byte = if idx + 1 == columns.len() && !trimmed.is_empty() {
2420 trimmed.len().saturating_sub(1) } else {
2422 let (end, next_start) =
2423 slice_cell_by_display_width(trimmed, pos_byte, columns[idx].width);
2424 pos_byte = next_start;
2425 end
2426 };
2427 let slice = &trimmed[start_byte..end_byte];
2428 if idx + 1 == columns.len() {
2429 pos_byte = trimmed.len();
2430 }
2431 slice
2432 } else {
2433 ""
2434 };
2435
2436 let cell_trimmed = part.trim();
2438 let ws_start_len = part.len() - part.trim_start().len();
2439 if ws_start_len > 0 {
2440 builder.token(SyntaxKind::WHITESPACE.into(), &part[..ws_start_len]);
2441 }
2442
2443 emit_table_cell(builder, cell_content, config);
2445
2446 let ws_end_start = ws_start_len + cell_trimmed.len();
2448 if ws_end_start < part.len() {
2449 builder.token(SyntaxKind::WHITESPACE.into(), &part[ws_end_start..]);
2450 }
2451
2452 if idx < cell_contents.len() - 1 || trimmed.ends_with('|') {
2454 builder.token(SyntaxKind::TEXT.into(), "|");
2455 }
2456 }
2457
2458 let trailing_ws_start = leading_ws_len + trimmed.len();
2460 if trailing_ws_start < line_without_newline.len() {
2461 builder.token(
2462 SyntaxKind::WHITESPACE.into(),
2463 &line_without_newline[trailing_ws_start..],
2464 );
2465 }
2466
2467 if !newline_str.is_empty() {
2469 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
2470 }
2471
2472 for &idx in &indices[1..] {
2475 let tail = window.emit_or_dispatch_tail(builder, idx);
2476 emit_line_tokens(builder, tail);
2477 }
2478
2479 builder.finish_node();
2480}
2481
2482pub(crate) fn try_parse_grid_table(
2485 window: &StrippedLines<'_, '_>,
2486 builder: &mut GreenNodeBuilder<'static>,
2487 config: &ParserOptions,
2488) -> Option<usize> {
2489 let lines = window.raw();
2490 let start_pos = window.pos();
2491 if start_pos >= lines.len() {
2492 return None;
2493 }
2494
2495 let view = UniformStripView(window);
2504
2505 if try_parse_grid_separator(view.line(start_pos)).is_none()
2511 && !is_caption_followed_by_table(&view, start_pos)
2512 {
2513 return None;
2514 }
2515
2516 let (actual_start, caption_before) = if is_caption_followed_by_table(&view, start_pos) {
2519 let (cap_start, cap_end) = caption_range_starting_at(&view, start_pos)?;
2520 let mut pos = cap_end;
2521 while pos < view.line_count() && view.line(pos).trim().is_empty() {
2522 pos += 1;
2523 }
2524 (pos, Some((cap_start, cap_end)))
2525 } else {
2526 (start_pos, None)
2527 };
2528
2529 if actual_start >= lines.len() {
2530 return None;
2531 }
2532
2533 let first_line = view.line(actual_start);
2535 let _columns = try_parse_grid_separator(first_line)?;
2536
2537 let mut end_pos = actual_start + 1;
2539 let mut found_header_sep = false;
2540 let mut in_footer = false;
2541
2542 while end_pos < lines.len() {
2544 let line = view.line(end_pos);
2545
2546 if line.trim().is_empty() {
2548 break;
2549 }
2550
2551 if let Some(sep_cols) = try_parse_grid_separator(line) {
2553 if sep_cols.iter().any(|c| c.is_header_separator) {
2555 if !found_header_sep {
2556 found_header_sep = true;
2557 } else if !in_footer {
2558 in_footer = true;
2560 }
2561 }
2562 end_pos += 1;
2563 continue;
2564 }
2565
2566 if is_grid_content_row(line) {
2568 end_pos += 1;
2569 continue;
2570 }
2571
2572 break;
2574 }
2575
2576 if end_pos <= actual_start + 1 {
2579 return None;
2580 }
2581
2582 let caption_before = caption_before.or_else(|| find_caption_before_table(&view, actual_start));
2587
2588 let caption_after = if caption_before.is_some() {
2590 None
2591 } else {
2592 find_caption_after_table(&view, end_pos)
2593 };
2594
2595 builder.start_node(SyntaxKind::GRID_TABLE.into());
2597
2598 if let Some((cap_start, cap_end)) = caption_before {
2600 emit_table_caption(builder, window, cap_start, cap_end, config);
2601 emit_caption_blank_lines(builder, window, cap_end, actual_start);
2603 }
2604
2605 let mut past_header_sep = false;
2607 let mut in_footer_section = false;
2608 let mut current_row_indices: Vec<usize> = Vec::new();
2611 let mut current_row_kind = SyntaxKind::TABLE_HEADER;
2612
2613 for idx in actual_start..end_pos {
2615 let line = view.line(idx);
2616 if let Some(sep_cols) = try_parse_grid_separator(line) {
2617 if !current_row_indices.is_empty() {
2619 emit_grid_table_row(
2620 builder,
2621 window,
2622 ¤t_row_indices,
2623 &sep_cols,
2624 current_row_kind,
2625 config,
2626 );
2627 current_row_indices.clear();
2628 }
2629
2630 let is_header_sep = sep_cols.iter().any(|c| c.is_header_separator);
2631
2632 if is_header_sep {
2635 if !past_header_sep {
2636 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
2638 let tail = window.emit_or_dispatch_tail(builder, idx);
2639 emit_separator_tokens(builder, tail);
2640 builder.finish_node();
2641 past_header_sep = true;
2642 } else {
2643 if !in_footer_section {
2645 in_footer_section = true;
2646 }
2647 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
2648 let tail = window.emit_or_dispatch_tail(builder, idx);
2649 emit_separator_tokens(builder, tail);
2650 builder.finish_node();
2651 }
2652 } else {
2653 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
2655 let tail = window.emit_or_dispatch_tail(builder, idx);
2656 emit_separator_tokens(builder, tail);
2657 builder.finish_node();
2658 }
2659 } else if is_grid_content_row(line) {
2660 current_row_kind = if !past_header_sep && found_header_sep {
2662 SyntaxKind::TABLE_HEADER
2663 } else if in_footer_section {
2664 SyntaxKind::TABLE_FOOTER
2665 } else {
2666 SyntaxKind::TABLE_ROW
2667 };
2668
2669 current_row_indices.push(idx);
2670 }
2671 }
2672
2673 if !current_row_indices.is_empty() {
2675 if let Some(sep_cols) = try_parse_grid_separator(view.line(actual_start)) {
2677 emit_grid_table_row(
2678 builder,
2679 window,
2680 ¤t_row_indices,
2681 &sep_cols,
2682 current_row_kind,
2683 config,
2684 );
2685 }
2686 }
2687
2688 if let Some((cap_start, cap_end)) = caption_after {
2690 emit_caption_blank_lines(builder, window, end_pos, cap_start);
2691 emit_table_caption(builder, window, cap_start, cap_end, config);
2692 }
2693
2694 builder.finish_node(); let table_start = caption_before
2698 .map(|(start, _)| start)
2699 .unwrap_or(actual_start);
2700 let table_end = if let Some((_, cap_end)) = caption_after {
2701 cap_end
2702 } else {
2703 end_pos
2704 };
2705
2706 Some(table_end - table_start)
2707}
2708
2709#[cfg(test)]
2710mod grid_table_tests {
2711 use super::super::container_prefix::ContainerPrefix;
2712 use super::*;
2713
2714 #[test]
2715 fn test_grid_separator_detection() {
2716 assert!(try_parse_grid_separator("+---+---+").is_some());
2717 assert!(try_parse_grid_separator("+===+===+").is_some());
2718 assert!(try_parse_grid_separator("+---------------+---------------+").is_some());
2719 assert!(try_parse_grid_separator("+:---:+").is_some()); assert!(try_parse_grid_separator("not a separator").is_none());
2721 assert!(try_parse_grid_separator("|---|---|").is_none()); assert!(try_parse_grid_separator(" +---+---+").is_none());
2728 assert!(try_parse_grid_separator(" +---+---+").is_none());
2729 assert!(try_parse_grid_separator(" +===+===+").is_none());
2730 }
2731
2732 #[test]
2733 fn test_grid_header_separator() {
2734 let cols = try_parse_grid_separator("+===+===+").unwrap();
2735 assert!(cols.iter().all(|c| c.is_header_separator));
2736
2737 let cols2 = try_parse_grid_separator("+---+---+").unwrap();
2738 assert!(cols2.iter().all(|c| !c.is_header_separator));
2739 }
2740
2741 #[test]
2742 fn test_grid_content_row_detection() {
2743 assert!(is_grid_content_row("| content | content |"));
2744 assert!(is_grid_content_row("| | |"));
2745 assert!(is_grid_content_row("| content +------+"));
2746 assert!(!is_grid_content_row("+---+---+")); assert!(!is_grid_content_row("no pipes here"));
2748 }
2749
2750 #[test]
2751 fn test_basic_grid_table() {
2752 let input = vec![
2753 "+-------+-------+",
2754 "| Col1 | Col2 |",
2755 "+=======+=======+",
2756 "| A | B |",
2757 "+-------+-------+",
2758 "",
2759 ];
2760
2761 let mut builder = GreenNodeBuilder::new();
2762 let prefix = ContainerPrefix::default();
2763 let window = StrippedLines::new(&input, 0, &prefix);
2764 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2765
2766 assert!(result.is_some());
2767 assert_eq!(result.unwrap(), 5);
2768 }
2769
2770 #[test]
2771 fn test_grid_table_multirow() {
2772 let input = vec![
2773 "+---------------+---------------+",
2774 "| Fruit | Advantages |",
2775 "+===============+===============+",
2776 "| Bananas | - wrapper |",
2777 "| | - color |",
2778 "+---------------+---------------+",
2779 "| Oranges | - scurvy |",
2780 "| | - tasty |",
2781 "+---------------+---------------+",
2782 "",
2783 ];
2784
2785 let mut builder = GreenNodeBuilder::new();
2786 let prefix = ContainerPrefix::default();
2787 let window = StrippedLines::new(&input, 0, &prefix);
2788 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2789
2790 assert!(result.is_some());
2791 assert_eq!(result.unwrap(), 9);
2792 }
2793
2794 #[test]
2795 fn test_grid_table_with_footer() {
2796 let input = vec![
2797 "+-------+-------+",
2798 "| Fruit | Price |",
2799 "+=======+=======+",
2800 "| Apple | $1.00 |",
2801 "+-------+-------+",
2802 "| Pear | $1.50 |",
2803 "+=======+=======+",
2804 "| Total | $2.50 |",
2805 "+=======+=======+",
2806 "",
2807 ];
2808
2809 let mut builder = GreenNodeBuilder::new();
2810 let prefix = ContainerPrefix::default();
2811 let window = StrippedLines::new(&input, 0, &prefix);
2812 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2813
2814 assert!(result.is_some());
2815 assert_eq!(result.unwrap(), 9);
2816 }
2817
2818 #[test]
2819 fn test_grid_table_headerless() {
2820 let input = vec![
2821 "+-------+-------+",
2822 "| A | B |",
2823 "+-------+-------+",
2824 "| C | D |",
2825 "+-------+-------+",
2826 "",
2827 ];
2828
2829 let mut builder = GreenNodeBuilder::new();
2830 let prefix = ContainerPrefix::default();
2831 let window = StrippedLines::new(&input, 0, &prefix);
2832 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2833
2834 assert!(result.is_some());
2835 assert_eq!(result.unwrap(), 5);
2836 }
2837
2838 #[test]
2839 fn test_grid_table_with_caption_before() {
2840 let input = vec![
2841 ": Sample table",
2842 "",
2843 "+-------+-------+",
2844 "| A | B |",
2845 "+=======+=======+",
2846 "| C | D |",
2847 "+-------+-------+",
2848 "",
2849 ];
2850
2851 let mut builder = GreenNodeBuilder::new();
2852 let prefix = ContainerPrefix::default();
2853 let window = StrippedLines::new(&input, 2, &prefix);
2854 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2855
2856 assert!(result.is_some());
2857 assert_eq!(result.unwrap(), 7);
2859 }
2860
2861 #[test]
2862 fn test_grid_table_with_caption_after() {
2863 let input = vec![
2864 "+-------+-------+",
2865 "| A | B |",
2866 "+=======+=======+",
2867 "| C | D |",
2868 "+-------+-------+",
2869 "",
2870 "Table: My grid table",
2871 "",
2872 ];
2873
2874 let mut builder = GreenNodeBuilder::new();
2875 let prefix = ContainerPrefix::default();
2876 let window = StrippedLines::new(&input, 0, &prefix);
2877 let result = try_parse_grid_table(&window, &mut builder, &ParserOptions::default());
2878
2879 assert!(result.is_some());
2880 assert_eq!(result.unwrap(), 7);
2882 }
2883}
2884
2885fn try_parse_multiline_separator(line: &str) -> Option<Vec<Column>> {
2893 let trimmed = line.trim_start();
2894 let leading_spaces = line.len() - trimmed.len();
2895
2896 if leading_spaces > 3 {
2898 return None;
2899 }
2900
2901 let trimmed = trimmed.trim_end();
2902
2903 if trimmed.is_empty() || !trimmed.chars().all(|c| c == '-') {
2905 return None;
2906 }
2907
2908 if trimmed.len() < 2 {
2913 return None;
2914 }
2915
2916 Some(vec![Column {
2918 start: leading_spaces,
2919 end: leading_spaces + trimmed.len(),
2920 alignment: Alignment::Default,
2921 }])
2922}
2923
2924fn is_column_separator(line: &str) -> bool {
2927 try_parse_table_separator(line).is_some() && !line.contains('*') && !line.contains('_')
2928}
2929
2930fn is_headerless_single_row_without_blank(
2931 lines: &(impl LineView + ?Sized),
2932 row_start: usize,
2933 row_end: usize,
2934 columns: &[Column],
2935) -> bool {
2936 if row_start >= row_end {
2937 return false;
2938 }
2939
2940 if row_end - row_start == 1 {
2941 return false;
2942 }
2943
2944 let Some(last_col) = columns.last() else {
2945 return false;
2946 };
2947
2948 for i in (row_start + 1)..row_end {
2949 let (content, _) = strip_newline(lines.line(i));
2950 let prefix_end = last_col.start.min(content.len());
2951 if !content[..prefix_end].trim().is_empty() {
2952 return false;
2953 }
2954 }
2955
2956 true
2957}
2958
2959pub(crate) fn try_parse_multiline_table(
2962 window: &StrippedLines<'_, '_>,
2963 builder: &mut GreenNodeBuilder<'static>,
2964 config: &ParserOptions,
2965) -> Option<usize> {
2966 let lines = window.raw();
2967 let start_pos = window.pos();
2968 if start_pos >= lines.len() {
2969 return None;
2970 }
2971
2972 let first_line = window.strip_at(start_pos);
2978
2979 let is_full_width_start = try_parse_multiline_separator(first_line).is_some();
2983 let is_column_sep_start = !is_full_width_start && is_column_separator(first_line);
2984 if !is_full_width_start && !is_column_sep_start {
2985 return None;
2986 }
2987
2988 let headerless_columns = if is_column_sep_start {
2997 try_parse_table_separator(window.line(start_pos))
2998 } else {
2999 None
3000 };
3001
3002 let opening_has_wide_column = headerless_columns
3009 .as_deref()
3010 .is_some_and(|cols| cols.iter().any(|col| col.end - col.start >= 2));
3011
3012 let mut pos = start_pos + 1;
3014 let mut found_column_sep = is_column_sep_start; let mut column_sep_pos = if is_column_sep_start { start_pos } else { 0 };
3016 let mut has_header = false;
3017 let mut found_blank_line = false;
3018 let mut found_closing_sep = false;
3019 let mut content_line_count = 0usize;
3020
3021 let bq_prefixed = window.prefix().bq_depth() > 0;
3028 let mut crossed_scope_boundary = false;
3029
3030 while pos < lines.len() {
3032 let line = window.line(pos);
3033
3034 if line_is_fenced_div_fence(line) || (bq_prefixed && lines[pos].trim().is_empty()) {
3035 crossed_scope_boundary = true;
3036 }
3037
3038 if is_full_width_start && is_column_separator(line) && !found_column_sep {
3040 found_column_sep = true;
3041 column_sep_pos = pos;
3042 has_header = pos > start_pos + 1; pos += 1;
3044 continue;
3045 }
3046
3047 if line.trim().is_empty() {
3049 found_blank_line = true;
3050 pos += 1;
3051 if pos < lines.len() {
3053 let next = window.line(pos);
3054 let is_valid_closer = if is_full_width_start {
3055 try_parse_multiline_separator(next).is_some()
3056 } else {
3057 is_column_separator(next)
3065 || (opening_has_wide_column
3066 && try_parse_multiline_separator(next).is_some())
3067 };
3068 if is_valid_closer {
3069 found_closing_sep = true;
3070 pos += 1; break;
3072 }
3073 }
3074 continue;
3075 }
3076
3077 if is_full_width_start && try_parse_multiline_separator(line).is_some() {
3079 found_closing_sep = true;
3080 pos += 1;
3081 break;
3082 }
3083
3084 if is_column_sep_start && is_column_separator(line) && content_line_count > 0 {
3086 found_closing_sep = true;
3087 pos += 1;
3088 break;
3089 }
3090
3091 content_line_count += 1;
3093 pos += 1;
3094 }
3095
3096 let first_row_adjacent =
3105 start_pos + 1 < lines.len() && !window.line(start_pos + 1).trim().is_empty();
3106 let headerless_single_column = !found_column_sep
3107 && is_full_width_start
3108 && first_row_adjacent
3109 && found_blank_line
3110 && found_closing_sep
3111 && !crossed_scope_boundary;
3112 if headerless_single_column {
3113 found_column_sep = true;
3114 column_sep_pos = start_pos;
3115 }
3116
3117 if !found_column_sep {
3119 return None;
3120 }
3121
3122 if !found_blank_line && is_column_sep_start {
3131 let columns = headerless_columns.as_deref()?;
3132 if !is_headerless_single_row_without_blank(window, start_pos + 1, pos - 1, columns) {
3133 return None;
3134 }
3135 }
3136
3137 if !found_closing_sep {
3139 return None;
3140 }
3141
3142 if pos <= start_pos + 2 {
3144 return None;
3145 }
3146
3147 let end_pos = pos;
3148
3149 let columns = try_parse_table_separator(window.line(column_sep_pos))
3154 .or_else(|| parse_single_dash_run(window.line(column_sep_pos)))?;
3155
3156 let caption_before = find_caption_before_table(window, start_pos);
3158
3159 let caption_after = if caption_before.is_some() {
3161 None
3162 } else {
3163 find_caption_after_table(window, end_pos)
3164 };
3165
3166 builder.start_node(SyntaxKind::MULTILINE_TABLE.into());
3168
3169 if let Some((cap_start, cap_end)) = caption_before {
3171 emit_table_caption(builder, window, cap_start, cap_end, config);
3172 emit_caption_blank_lines(builder, window, cap_end, start_pos);
3174 }
3175
3176 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
3180 let tail = window.emit_or_dispatch_tail(builder, start_pos);
3181 emit_separator_tokens(builder, tail);
3182 builder.finish_node();
3183
3184 let mut in_header = has_header;
3188 let mut current_row_indices: Vec<usize> = Vec::new();
3189
3190 for i in (start_pos + 1)..end_pos {
3191 let line = window.line(i);
3192 if i == column_sep_pos {
3194 if !current_row_indices.is_empty() {
3196 emit_multiline_table_row(
3197 builder,
3198 window,
3199 ¤t_row_indices,
3200 &columns,
3201 SyntaxKind::TABLE_HEADER,
3202 config,
3203 );
3204 current_row_indices.clear();
3205 }
3206
3207 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
3208 let tail = window.emit_or_dispatch_tail(builder, i);
3209 emit_separator_tokens(builder, tail);
3210 builder.finish_node();
3211 in_header = false;
3212 continue;
3213 }
3214
3215 if try_parse_multiline_separator(line).is_some() || is_column_separator(line) {
3217 if !current_row_indices.is_empty() {
3219 let kind = if in_header {
3220 SyntaxKind::TABLE_HEADER
3221 } else {
3222 SyntaxKind::TABLE_ROW
3223 };
3224 emit_multiline_table_row(
3225 builder,
3226 window,
3227 ¤t_row_indices,
3228 &columns,
3229 kind,
3230 config,
3231 );
3232 current_row_indices.clear();
3233 }
3234
3235 builder.start_node(SyntaxKind::TABLE_SEPARATOR.into());
3236 let tail = window.emit_or_dispatch_tail(builder, i);
3237 emit_separator_tokens(builder, tail);
3238 builder.finish_node();
3239 continue;
3240 }
3241
3242 if line.trim().is_empty() {
3244 if !current_row_indices.is_empty() {
3246 let kind = if in_header {
3247 SyntaxKind::TABLE_HEADER
3248 } else {
3249 SyntaxKind::TABLE_ROW
3250 };
3251 emit_multiline_table_row(
3252 builder,
3253 window,
3254 ¤t_row_indices,
3255 &columns,
3256 kind,
3257 config,
3258 );
3259 current_row_indices.clear();
3260 }
3261
3262 builder.start_node(SyntaxKind::BLANK_LINE.into());
3265 let tail = window.emit_or_dispatch_tail(builder, i);
3266 builder.token(SyntaxKind::BLANK_LINE.into(), tail);
3267 builder.finish_node();
3268 continue;
3269 }
3270
3271 current_row_indices.push(i);
3273 }
3274
3275 if !current_row_indices.is_empty() {
3277 let kind = if in_header {
3278 SyntaxKind::TABLE_HEADER
3279 } else {
3280 SyntaxKind::TABLE_ROW
3281 };
3282 emit_multiline_table_row(
3283 builder,
3284 window,
3285 ¤t_row_indices,
3286 &columns,
3287 kind,
3288 config,
3289 );
3290 }
3291
3292 if let Some((cap_start, cap_end)) = caption_after {
3294 emit_caption_blank_lines(builder, window, end_pos, cap_start);
3295 emit_table_caption(builder, window, cap_start, cap_end, config);
3296 }
3297
3298 builder.finish_node(); let table_start = caption_before.map(|(start, _)| start).unwrap_or(start_pos);
3302 let table_end = if let Some((_, cap_end)) = caption_after {
3303 cap_end
3304 } else {
3305 end_pos
3306 };
3307
3308 Some(table_end - table_start)
3309}
3310
3311fn extract_first_line_cell_contents(line: &str, columns: &[Column]) -> Vec<String> {
3314 let (line_content, _) = strip_newline(line);
3315 let mut cells = Vec::new();
3316
3317 for (i, column) in columns.iter().enumerate() {
3318 let column_start = column_offset_to_byte_index(line_content, column.start);
3319 let column_end = if i + 1 == columns.len() {
3323 line_content.len().max(column_start)
3324 } else {
3325 column_offset_to_byte_index(line_content, column.end)
3326 };
3327
3328 let cell_text = if column_start < column_end {
3330 &line_content[column_start..column_end]
3331 } else if column_start < line_content.len() {
3332 &line_content[column_start..]
3333 } else {
3334 ""
3335 };
3336
3337 cells.push(cell_text.to_string());
3338 }
3339
3340 cells
3341}
3342
3343fn emit_multiline_table_row(
3350 builder: &mut GreenNodeBuilder<'static>,
3351 window: &StrippedLines<'_, '_>,
3352 indices: &[usize],
3353 columns: &[Column],
3354 kind: SyntaxKind,
3355 config: &ParserOptions,
3356) {
3357 if indices.is_empty() {
3358 return;
3359 }
3360
3361 builder.start_node(kind.into());
3362
3363 let first_line = window.emit_or_dispatch_tail(builder, indices[0]);
3367 let cell_contents = extract_first_line_cell_contents(first_line, columns);
3368 let (trimmed, newline_str) = strip_newline(first_line);
3369 let mut current_pos = 0;
3370
3371 for (col_idx, column) in columns.iter().enumerate() {
3372 let cell_text = &cell_contents[col_idx];
3373 let cell_start = column_offset_to_byte_index(trimmed, column.start);
3374 let cell_end = if col_idx + 1 == columns.len() {
3377 trimmed.len().max(cell_start)
3378 } else {
3379 column_offset_to_byte_index(trimmed, column.end)
3380 };
3381
3382 if current_pos < cell_start {
3384 builder.token(
3385 SyntaxKind::WHITESPACE.into(),
3386 &trimmed[current_pos..cell_start],
3387 );
3388 }
3389
3390 emit_table_cell(builder, cell_text, config);
3392
3393 current_pos = cell_end;
3394 }
3395
3396 if current_pos < trimmed.len() {
3398 builder.token(SyntaxKind::WHITESPACE.into(), &trimmed[current_pos..]);
3399 }
3400
3401 if !newline_str.is_empty() {
3403 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
3404 }
3405
3406 for &idx in &indices[1..] {
3409 let tail = window.emit_or_dispatch_tail(builder, idx);
3410 emit_line_tokens(builder, tail);
3411 }
3412
3413 builder.finish_node();
3414}
3415
3416#[cfg(test)]
3417mod multiline_table_tests {
3418 use super::super::container_prefix::ContainerPrefix;
3419 use super::*;
3420 use crate::syntax::SyntaxNode;
3421
3422 #[test]
3423 fn test_multiline_separator_detection() {
3424 assert!(
3425 try_parse_multiline_separator(
3426 "-------------------------------------------------------------"
3427 )
3428 .is_some()
3429 );
3430 assert!(try_parse_multiline_separator("---").is_some());
3431 assert!(try_parse_multiline_separator(" -----").is_some()); 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()); }
3437
3438 #[test]
3439 fn test_basic_multiline_table() {
3440 let input = vec![
3441 "-------------------------------------------------------------",
3442 " Centered Default Right Left",
3443 " Header Aligned Aligned Aligned",
3444 "----------- ------- --------------- -------------------------",
3445 " First row 12.0 Example of a row that",
3446 " spans multiple lines.",
3447 "",
3448 " Second row 5.0 Here's another one.",
3449 "-------------------------------------------------------------",
3450 "",
3451 ];
3452
3453 let mut builder = GreenNodeBuilder::new();
3454 let prefix = ContainerPrefix::default();
3455 let window = StrippedLines::new(&input, 0, &prefix);
3456 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3457
3458 assert!(result.is_some());
3459 assert_eq!(result.unwrap(), 9);
3460 }
3461
3462 #[test]
3463 fn test_multiline_table_headerless() {
3464 let input = vec![
3465 "----------- ------- --------------- -------------------------",
3466 " First row 12.0 Example of a row that",
3467 " spans multiple lines.",
3468 "",
3469 " Second row 5.0 Here's another one.",
3470 "----------- ------- --------------- -------------------------",
3471 "",
3472 ];
3473
3474 let mut builder = GreenNodeBuilder::new();
3475 let prefix = ContainerPrefix::default();
3476 let window = StrippedLines::new(&input, 0, &prefix);
3477 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3478
3479 assert!(result.is_some());
3480 assert_eq!(result.unwrap(), 6);
3481 }
3482
3483 #[test]
3484 fn test_multiline_table_headerless_single_line_is_not_multiline() {
3485 let input = vec![
3486 "------- ------ ---------- -------",
3487 " 12 12 12 12",
3488 "------- ------ ---------- -------",
3489 "",
3490 "Not part of table.",
3491 "",
3492 ];
3493
3494 let mut builder = GreenNodeBuilder::new();
3495 let prefix = ContainerPrefix::default();
3496 let window = StrippedLines::new(&input, 0, &prefix);
3497 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3498
3499 assert!(result.is_none());
3500 }
3501
3502 #[test]
3503 fn test_multiline_table_headerless_single_row_continuation_without_blank_line() {
3504 let input = vec![
3505 "---------- --------- ----------- ---------------------------",
3506 " First row 12.0 Example of a row that spans",
3507 " multiple lines.",
3508 "---------- --------- ----------- ---------------------------",
3509 "",
3510 ];
3511
3512 let mut builder = GreenNodeBuilder::new();
3513 let prefix = ContainerPrefix::default();
3514 let window = StrippedLines::new(&input, 0, &prefix);
3515 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3516
3517 assert!(result.is_some());
3518 assert_eq!(result.unwrap(), 4);
3519 }
3520
3521 #[test]
3522 fn test_multiline_table_with_caption() {
3523 let input = vec![
3524 "-------------------------------------------------------------",
3525 " Col1 Col2",
3526 "----------- -------",
3527 " A B",
3528 "",
3529 "-------------------------------------------------------------",
3530 "",
3531 "Table: Here's the caption.",
3532 "",
3533 ];
3534
3535 let mut builder = GreenNodeBuilder::new();
3536 let prefix = ContainerPrefix::default();
3537 let window = StrippedLines::new(&input, 0, &prefix);
3538 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3539
3540 assert!(result.is_some());
3541 assert_eq!(result.unwrap(), 8);
3543 }
3544
3545 #[test]
3546 fn test_multiline_table_single_row() {
3547 let input = vec![
3548 "---------------------------------------------",
3549 " Header1 Header2",
3550 "----------- -----------",
3551 " Data More data",
3552 "",
3553 "---------------------------------------------",
3554 "",
3555 ];
3556
3557 let mut builder = GreenNodeBuilder::new();
3558 let prefix = ContainerPrefix::default();
3559 let window = StrippedLines::new(&input, 0, &prefix);
3560 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3561
3562 assert!(result.is_some());
3563 assert_eq!(result.unwrap(), 6);
3564 }
3565
3566 #[test]
3567 fn test_headerless_multiline_table_does_not_close_on_full_width_rule() {
3568 let input = vec![
3569 "- - - - -",
3570 "Third section with underscores.",
3571 "",
3572 "_____",
3573 "",
3574 "> Quote before rule",
3575 ">",
3576 "> ***",
3577 ">",
3578 "> Quote after rule",
3579 "",
3580 "Final paragraph.",
3581 "",
3582 "Here's a horizontal rule:",
3583 "",
3584 "---",
3585 "Text directly after the horizontal rule.",
3586 "",
3587 ];
3588
3589 let mut builder = GreenNodeBuilder::new();
3590 let prefix = ContainerPrefix::default();
3591 let window = StrippedLines::new(&input, 0, &prefix);
3592 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3593
3594 assert!(result.is_none());
3595 }
3596
3597 #[test]
3598 fn test_not_multiline_table() {
3599 let input = vec![
3601 " Right Left Center Default",
3602 "------- ------ ---------- -------",
3603 " 12 12 12 12",
3604 "",
3605 ];
3606
3607 let mut builder = GreenNodeBuilder::new();
3608 let prefix = ContainerPrefix::default();
3609 let window = StrippedLines::new(&input, 0, &prefix);
3610 let result = try_parse_multiline_table(&window, &mut builder, &ParserOptions::default());
3611
3612 assert!(result.is_none());
3614 }
3615
3616 #[test]
3618 fn test_emit_table_cell_plain_text() {
3619 let mut builder = GreenNodeBuilder::new();
3620 emit_table_cell(&mut builder, "Cell", &ParserOptions::default());
3621 let green = builder.finish();
3622 let node = SyntaxNode::new_root(green);
3623
3624 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3625 assert_eq!(node.text(), "Cell");
3626
3627 let children: Vec<_> = node.children_with_tokens().collect();
3629 assert_eq!(children.len(), 1);
3630 assert_eq!(children[0].kind(), SyntaxKind::TEXT);
3631 }
3632
3633 #[test]
3634 fn test_emit_table_cell_with_emphasis() {
3635 let mut builder = GreenNodeBuilder::new();
3636 emit_table_cell(&mut builder, "*italic*", &ParserOptions::default());
3637 let green = builder.finish();
3638 let node = SyntaxNode::new_root(green);
3639
3640 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3641 assert_eq!(node.text(), "*italic*");
3642
3643 let children: Vec<_> = node.children().collect();
3645 assert_eq!(children.len(), 1);
3646 assert_eq!(children[0].kind(), SyntaxKind::EMPHASIS);
3647 }
3648
3649 #[test]
3650 fn test_emit_table_cell_with_code() {
3651 let mut builder = GreenNodeBuilder::new();
3652 emit_table_cell(&mut builder, "`code`", &ParserOptions::default());
3653 let green = builder.finish();
3654 let node = SyntaxNode::new_root(green);
3655
3656 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3657 assert_eq!(node.text(), "`code`");
3658
3659 let children: Vec<_> = node.children().collect();
3661 assert_eq!(children.len(), 1);
3662 assert_eq!(children[0].kind(), SyntaxKind::INLINE_CODE);
3663 }
3664
3665 #[test]
3666 fn test_emit_table_cell_with_link() {
3667 let mut builder = GreenNodeBuilder::new();
3668 emit_table_cell(&mut builder, "[text](url)", &ParserOptions::default());
3669 let green = builder.finish();
3670 let node = SyntaxNode::new_root(green);
3671
3672 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3673 assert_eq!(node.text(), "[text](url)");
3674
3675 let children: Vec<_> = node.children().collect();
3677 assert_eq!(children.len(), 1);
3678 assert_eq!(children[0].kind(), SyntaxKind::LINK);
3679 }
3680
3681 #[test]
3682 fn test_emit_table_cell_with_strong() {
3683 let mut builder = GreenNodeBuilder::new();
3684 emit_table_cell(&mut builder, "**bold**", &ParserOptions::default());
3685 let green = builder.finish();
3686 let node = SyntaxNode::new_root(green);
3687
3688 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3689 assert_eq!(node.text(), "**bold**");
3690
3691 let children: Vec<_> = node.children().collect();
3693 assert_eq!(children.len(), 1);
3694 assert_eq!(children[0].kind(), SyntaxKind::STRONG);
3695 }
3696
3697 #[test]
3698 fn test_emit_table_cell_mixed_inline() {
3699 let mut builder = GreenNodeBuilder::new();
3700 emit_table_cell(
3701 &mut builder,
3702 "Text **bold** and `code`",
3703 &ParserOptions::default(),
3704 );
3705 let green = builder.finish();
3706 let node = SyntaxNode::new_root(green);
3707
3708 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3709 assert_eq!(node.text(), "Text **bold** and `code`");
3710
3711 let children: Vec<_> = node.children_with_tokens().collect();
3713 assert!(children.len() >= 4);
3714
3715 assert_eq!(children[0].kind(), SyntaxKind::TEXT);
3717 assert_eq!(children[1].kind(), SyntaxKind::STRONG);
3718 }
3719
3720 #[test]
3721 fn test_emit_table_cell_empty() {
3722 let mut builder = GreenNodeBuilder::new();
3723 emit_table_cell(&mut builder, "", &ParserOptions::default());
3724 let green = builder.finish();
3725 let node = SyntaxNode::new_root(green);
3726
3727 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3728 assert_eq!(node.text(), "");
3729
3730 let children: Vec<_> = node.children_with_tokens().collect();
3732 assert_eq!(children.len(), 0);
3733 }
3734
3735 #[test]
3736 fn test_emit_table_cell_escaped_pipe() {
3737 let mut builder = GreenNodeBuilder::new();
3738 emit_table_cell(&mut builder, r"A \| B", &ParserOptions::default());
3739 let green = builder.finish();
3740 let node = SyntaxNode::new_root(green);
3741
3742 assert_eq!(node.kind(), SyntaxKind::TABLE_CELL);
3743 assert_eq!(node.text(), r"A \| B");
3745 }
3746}