oxc_markdown_parser/block/
lexical.rs1use crate::options::Constructs;
16use crate::syntax;
17
18use super::cursor::Cursor;
19use super::probe::{Start, probe};
20use super::scan;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
26#[non_exhaustive]
27pub enum LineStart {
28 Blockquote,
29 SetextUnderline,
31 ThematicBreak,
32 AtxHeading,
33 CodeFence,
34 HtmlBlock,
35 MathFence,
36 Liquid,
37 DirectiveOpener,
39 DirectiveCloser,
41 FootnoteDefinition,
42 ListItem,
43 TableDelimiterRow,
46 TableRow,
48}
49
50pub fn ordered_number(marker: &str) -> Option<u32> {
53 marker.trim_end_matches(['.', ')']).parse().ok()
54}
55
56pub fn line_start(constructs: &Constructs, line: &str, in_paragraph: bool) -> Option<LineStart> {
63 let tail = flow_tail(line)?;
64 if let Some(start) = probe(constructs, tail, in_paragraph) {
65 return Some(match start {
66 Start::Quote => LineStart::Blockquote,
67 Start::Setext(_) => LineStart::SetextUnderline,
68 Start::ThematicBreak => LineStart::ThematicBreak,
69 Start::Atx { .. } => LineStart::AtxHeading,
70 Start::Fence { .. } => LineStart::CodeFence,
71 Start::Html { .. } => LineStart::HtmlBlock,
72 Start::MathFence { .. } => LineStart::MathFence,
73 Start::Liquid => {
74 let closer = syntax::liquid::open(tail)?;
77 if let Some(after) = syntax::liquid::close(&tail[2..], closer)
78 && !scan::is_blank(&tail[2 + after..])
79 {
80 return None;
81 }
82 LineStart::Liquid
83 }
84 Start::Directive { .. } => LineStart::DirectiveOpener,
85 Start::FootnoteDef { .. } => LineStart::FootnoteDefinition,
86 Start::ListItem { .. } => LineStart::ListItem,
87 Start::TableDelimiter(_) => LineStart::TableDelimiterRow,
88 });
89 }
90 if constructs.container_directive && scan::fence_close(tail, b':', 3) {
94 return Some(LineStart::DirectiveCloser);
95 }
96 if constructs.gfm_table && tail.starts_with('|') {
97 return Some(LineStart::TableRow);
98 }
99 None
100}
101
102pub fn table_delimiter_activates(constructs: &Constructs, header: &str, row: &str) -> bool {
108 flow_tail(header).is_some()
109 && matches!(
110 flow_tail(row).and_then(|tail| probe(constructs, tail, true)),
111 Some(Start::TableDelimiter(align)) if scan::table_row_cells(header).len() == align.len()
112 )
113}
114
115fn flow_tail(line: &str) -> Option<&str> {
117 Cursor::new(line, 0).flow_start().map(|cursor| cursor.tail())
118}
119
120#[cfg(test)]
121mod tests {
122 use super::*;
123
124 fn md(line: &str, in_paragraph: bool) -> Option<LineStart> {
125 line_start(&Constructs::markdown(), line, in_paragraph)
126 }
127
128 #[test]
129 fn probe_table_mirrors_engine() {
130 assert_eq!(md("> q", false), Some(LineStart::Blockquote));
131 assert_eq!(md("---", true), Some(LineStart::SetextUnderline));
132 assert_eq!(md("---", false), Some(LineStart::ThematicBreak));
133 assert_eq!(md("# h", false), Some(LineStart::AtxHeading));
134 assert_eq!(md("```rs", false), Some(LineStart::CodeFence));
135 assert_eq!(md("<div>", false), Some(LineStart::HtmlBlock));
136 assert_eq!(md("$$", false), Some(LineStart::MathFence));
137 assert_eq!(md("{% t %}", false), Some(LineStart::Liquid));
138 assert_eq!(md("{% t", false), Some(LineStart::Liquid));
139 assert_eq!(md("{{ t }} text", false), None);
140 assert_eq!(md("[^1]: n", false), Some(LineStart::FootnoteDefinition));
141 assert_eq!(md("- item", false), Some(LineStart::ListItem));
142 assert_eq!(md("plain text", true), None);
143 }
144
145 #[test]
146 fn ordered_numbers() {
147 assert_eq!(ordered_number("007."), Some(7));
148 assert_eq!(ordered_number("999999999)"), Some(999_999_999));
149 assert_eq!(ordered_number("-"), None);
150 }
151
152 #[test]
153 fn covers_prettiers_documented_misses() {
154 assert_eq!(md(":::note", false), Some(LineStart::DirectiveOpener));
156 assert_eq!(md(":::", false), Some(LineStart::DirectiveCloser));
157 assert_eq!(md(":::)", false), None, "an emoticon is paragraph text");
158 assert_eq!(md("| - | - |", true), Some(LineStart::TableDelimiterRow));
159 assert_eq!(md("| a | b |", false), Some(LineStart::TableRow));
160 }
161
162 #[test]
163 fn table_delimiter_activates_on_matching_cells() {
164 let md = Constructs::markdown();
165 assert!(table_delimiter_activates(&md, "| a | b |", "| - | - |"));
166 assert!(table_delimiter_activates(&md, "a | b", "--- | ---"));
167 assert!(
168 !table_delimiter_activates(&md, "| a | b |", "| - | - | - |"),
169 "a cell count mismatch"
170 );
171 assert!(!table_delimiter_activates(&md, "| a | b |", "| a | b |"), "not a delimiter row");
172 assert!(
173 !table_delimiter_activates(&md, " | a | b |", "| - | - |"),
174 "an indented header"
175 );
176 }
177
178 #[test]
179 fn indent_is_indented_code() {
180 assert_eq!(md(" # not a heading", false), None);
181 assert_eq!(md("\t- not a list", false), None);
182 assert_eq!(md(" \t- not a list", false), None);
183 assert_eq!(md(" - list", false), Some(LineStart::ListItem));
184 }
185
186 #[test]
189 fn construct_gating() {
190 let mut c = Constructs::markdown();
191 c.container_directive = false;
192 c.gfm_table = false;
193 assert_eq!(line_start(&c, ":::", false), None);
194 assert_eq!(line_start(&c, "| a |", false), None);
195 }
196}