panache_parser/parser/blocks/
definition_lists.rs1use crate::options::ParserOptions;
2use crate::syntax::SyntaxKind;
3use rowan::GreenNodeBuilder;
4
5use crate::parser::utils::container_stack::leading_indent;
6use crate::parser::utils::helpers::strip_newline;
7use crate::parser::utils::inline_emission;
8
9pub(crate) fn try_parse_definition_marker(line: &str) -> Option<(char, usize, usize, usize)> {
14 {
19 let bytes = line.as_bytes();
20 let mut i = 0;
21 while i < bytes.len() && i < 3 && bytes[i] == b' ' {
22 i += 1;
23 }
24 match bytes.get(i) {
25 Some(&b':') | Some(&b'~') => {}
26 _ => return None,
27 }
28 }
29
30 let (indent_cols, indent_bytes) = leading_indent(line);
32 if indent_cols > 3 {
33 return None;
34 }
35
36 let after_indent = &line[indent_bytes..];
37
38 let marker = after_indent.chars().next()?;
40 if !matches!(marker, ':' | '~') {
41 return None;
42 }
43
44 let after_marker = &after_indent[1..];
45
46 if !after_marker.starts_with(' ') && !after_marker.starts_with('\t') && !after_marker.is_empty()
48 {
49 return None;
50 }
51
52 let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
53
54 Some((marker, indent_cols, spaces_after_cols, spaces_after_bytes))
55}
56
57pub(crate) fn emit_term(
59 builder: &mut GreenNodeBuilder<'static>,
60 line: &str,
61 config: &ParserOptions,
62) {
63 builder.start_node(SyntaxKind::TERM.into());
64 let (text, newline_str) = strip_newline(line);
66 let trimmed_text = text.trim_end();
67
68 if !trimmed_text.is_empty() {
69 inline_emission::emit_inlines(builder, trimmed_text, config, false);
70 }
71
72 if !newline_str.is_empty() {
73 builder.token(SyntaxKind::NEWLINE.into(), newline_str);
74 }
75 builder.finish_node(); }
77
78pub(crate) fn emit_definition_marker(
80 builder: &mut GreenNodeBuilder<'static>,
81 marker: char,
82 indent_cols: usize,
83) {
84 if indent_cols > 0 {
85 builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent_cols));
86 }
87 builder.token(SyntaxKind::DEFINITION_MARKER.into(), &marker.to_string());
88}
89
90use crate::parser::blocks::tables::is_caption_followed_by_table;
93use crate::parser::utils::container_stack::{Container, ContainerStack};
94
95pub(in crate::parser) fn in_definition_list(containers: &ContainerStack) -> bool {
97 containers
98 .stack
99 .iter()
100 .any(|c| matches!(c, Container::DefinitionList { .. }))
101}
102
103pub(in crate::parser) fn next_line_is_definition_marker(
106 lines: &[&str],
107 pos: usize,
108) -> Option<usize> {
109 let mut check_pos = pos + 1;
110 let mut blank_count = 0;
111 while check_pos < lines.len() {
112 let line = lines[check_pos];
113 if line.trim().is_empty() {
114 blank_count += 1;
115 check_pos += 1;
116 continue;
117 }
118 if try_parse_definition_marker(line).is_some() {
119 if let Some((marker, ..)) = try_parse_definition_marker(line)
125 && marker == ':'
126 && is_caption_followed_by_table(lines, check_pos)
127 {
128 return None;
129 }
130 return Some(blank_count);
131 } else {
132 return None;
133 }
134 }
135 None
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141 use crate::parser::blocks::tables::is_caption_followed_by_table;
142
143 #[test]
144 fn test_parse_definition_marker_colon() {
145 assert_eq!(
146 try_parse_definition_marker(": Definition"),
147 Some((':', 0, 3, 3))
148 );
149 }
150
151 #[test]
152 fn test_parse_definition_marker_tilde() {
153 assert_eq!(
154 try_parse_definition_marker("~ Definition"),
155 Some(('~', 0, 3, 3))
156 );
157 }
158
159 #[test]
160 fn test_parse_definition_marker_indented() {
161 assert_eq!(
162 try_parse_definition_marker(" : Definition"),
163 Some((':', 2, 1, 1))
164 );
165 assert_eq!(
166 try_parse_definition_marker(" ~ Definition"),
167 Some(('~', 3, 1, 1))
168 );
169 assert_eq!(try_parse_definition_marker("\t: Definition"), None);
170 }
171
172 #[test]
173 fn test_parse_definition_marker_too_indented() {
174 assert_eq!(try_parse_definition_marker(" : Definition"), None);
175 }
176
177 #[test]
178 fn test_parse_definition_marker_no_space_after() {
179 assert_eq!(try_parse_definition_marker(":Definition"), None);
180 }
181
182 #[test]
183 fn test_parse_definition_marker_at_eol() {
184 assert_eq!(try_parse_definition_marker(":"), Some((':', 0, 0, 0)));
185 }
186
187 #[test]
188 fn next_line_marker_ignores_colon_table_caption() {
189 let lines = vec![
190 "Here's a table with a reference:",
191 "",
192 ": (\\#tab:mytable) A table with a reference.",
193 "",
194 "| A | B | C |",
195 "| --- | --- | --- |",
196 "| 1 | 2 | 3 |",
197 ];
198 assert!(is_caption_followed_by_table(&lines[..], 2));
199 assert_eq!(next_line_is_definition_marker(&lines, 0), None);
200 }
201
202 #[test]
203 fn test_definition_list_preserves_first_content_line_losslessly() {
204 let input = "[`--reference-doc=`*FILE*]{#option--reference-doc}\n\n: Use the specified file as a style reference in producing a\n docx or ODT file.\n\n Docx\n\n : For best results, the reference docx should be a modified\n version of a docx file produced using pandoc.\n";
205 let tree = crate::parse(input, Some(crate::ParserOptions::default()));
206 assert_eq!(tree.text().to_string(), input);
207 }
208}