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