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)
120 && marker == ':'
121 && is_caption_followed_by_table(lines, check_pos)
122 {
123 return None;
124 }
125 return Some(blank_count);
126 } else {
127 return None;
128 }
129 }
130 None
131}
132
133#[cfg(test)]
134mod tests {
135 use super::*;
136 use crate::parser::blocks::tables::is_caption_followed_by_table;
137
138 #[test]
139 fn test_parse_definition_marker_colon() {
140 assert_eq!(
141 try_parse_definition_marker(": Definition"),
142 Some((':', 0, 3, 3))
143 );
144 }
145
146 #[test]
147 fn test_parse_definition_marker_tilde() {
148 assert_eq!(
149 try_parse_definition_marker("~ Definition"),
150 Some(('~', 0, 3, 3))
151 );
152 }
153
154 #[test]
155 fn test_parse_definition_marker_indented() {
156 assert_eq!(
157 try_parse_definition_marker(" : Definition"),
158 Some((':', 2, 1, 1))
159 );
160 assert_eq!(
161 try_parse_definition_marker(" ~ Definition"),
162 Some(('~', 3, 1, 1))
163 );
164 assert_eq!(try_parse_definition_marker("\t: Definition"), None);
165 }
166
167 #[test]
168 fn test_parse_definition_marker_too_indented() {
169 assert_eq!(try_parse_definition_marker(" : Definition"), None);
170 }
171
172 #[test]
173 fn test_parse_definition_marker_no_space_after() {
174 assert_eq!(try_parse_definition_marker(":Definition"), None);
175 }
176
177 #[test]
178 fn test_parse_definition_marker_at_eol() {
179 assert_eq!(try_parse_definition_marker(":"), Some((':', 0, 0, 0)));
180 }
181
182 #[test]
183 fn next_line_marker_ignores_colon_table_caption() {
184 let lines = vec![
185 "Here's a table with a reference:",
186 "",
187 ": (\\#tab:mytable) A table with a reference.",
188 "",
189 "| A | B | C |",
190 "| --- | --- | --- |",
191 "| 1 | 2 | 3 |",
192 ];
193 assert!(is_caption_followed_by_table(&lines, 2));
194 assert_eq!(next_line_is_definition_marker(&lines, 0), None);
195 }
196
197 #[test]
198 fn test_definition_list_preserves_first_content_line_losslessly() {
199 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";
200 let tree = crate::parse(input, Some(crate::ParserOptions::default()));
201 assert_eq!(tree.text().to_string(), input);
202 }
203}