Skip to main content

panache_parser/parser/blocks/
definition_lists.rs

1use 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
9/// Tries to parse a definition list marker (`:` or `~`)
10///
11/// Returns Some((marker_char, indent_cols, spaces_after_cols, spaces_after_bytes)) if found, None otherwise.
12/// The marker can be indented 0-3 spaces and must be followed by whitespace.
13pub(crate) fn try_parse_definition_marker(line: &str) -> Option<(char, usize, usize, usize)> {
14    // Count leading whitespace in columns (0-3 allowed)
15    let (indent_cols, indent_bytes) = leading_indent(line);
16    if indent_cols > 3 {
17        return None;
18    }
19
20    let after_indent = &line[indent_bytes..];
21
22    // Check for : or ~ marker
23    let marker = after_indent.chars().next()?;
24    if !matches!(marker, ':' | '~') {
25        return None;
26    }
27
28    let after_marker = &after_indent[1..];
29
30    // Must be followed by whitespace
31    if !after_marker.starts_with(' ') && !after_marker.starts_with('\t') && !after_marker.is_empty()
32    {
33        return None;
34    }
35
36    let (spaces_after_cols, spaces_after_bytes) = leading_indent(after_marker);
37
38    Some((marker, indent_cols, spaces_after_cols, spaces_after_bytes))
39}
40
41/// Emit a term line into the syntax tree
42pub(crate) fn emit_term(
43    builder: &mut GreenNodeBuilder<'static>,
44    line: &str,
45    config: &ParserOptions,
46) {
47    builder.start_node(SyntaxKind::TERM.into());
48    // Strip trailing newline from line (it will be emitted separately)
49    let (text, newline_str) = strip_newline(line);
50    let trimmed_text = text.trim_end();
51
52    if !trimmed_text.is_empty() {
53        inline_emission::emit_inlines(builder, trimmed_text, config);
54    }
55
56    if !newline_str.is_empty() {
57        builder.token(SyntaxKind::NEWLINE.into(), newline_str);
58    }
59    builder.finish_node(); // Term
60}
61
62/// Emit a definition marker
63pub(crate) fn emit_definition_marker(
64    builder: &mut GreenNodeBuilder<'static>,
65    marker: char,
66    indent_cols: usize,
67) {
68    if indent_cols > 0 {
69        builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent_cols));
70    }
71    builder.token(SyntaxKind::DEFINITION_MARKER.into(), &marker.to_string());
72}
73
74// Helper functions for definition list management in Parser
75
76use crate::parser::blocks::tables::is_caption_followed_by_table;
77use crate::parser::utils::container_stack::{Container, ContainerStack};
78
79/// Check if we're in a definition list.
80pub(in crate::parser) fn in_definition_list(containers: &ContainerStack) -> bool {
81    containers
82        .stack
83        .iter()
84        .any(|c| matches!(c, Container::DefinitionList { .. }))
85}
86
87/// Look ahead past blank lines to find a definition marker.
88/// Returns Some(blank_line_count) if found, None otherwise.
89pub(in crate::parser) fn next_line_is_definition_marker(
90    lines: &[&str],
91    pos: usize,
92) -> Option<usize> {
93    let mut check_pos = pos + 1;
94    let mut blank_count = 0;
95    while check_pos < lines.len() {
96        let line = lines[check_pos];
97        if line.trim().is_empty() {
98            blank_count += 1;
99            check_pos += 1;
100            continue;
101        }
102        if try_parse_definition_marker(line).is_some() {
103            if let Some((marker, ..)) = try_parse_definition_marker(line)
104                && marker == ':'
105                && is_caption_followed_by_table(lines, check_pos)
106            {
107                return None;
108            }
109            return Some(blank_count);
110        } else {
111            return None;
112        }
113    }
114    None
115}
116
117#[cfg(test)]
118mod tests {
119    use super::*;
120    use crate::parser::blocks::tables::is_caption_followed_by_table;
121
122    #[test]
123    fn test_parse_definition_marker_colon() {
124        assert_eq!(
125            try_parse_definition_marker(":   Definition"),
126            Some((':', 0, 3, 3))
127        );
128    }
129
130    #[test]
131    fn test_parse_definition_marker_tilde() {
132        assert_eq!(
133            try_parse_definition_marker("~   Definition"),
134            Some(('~', 0, 3, 3))
135        );
136    }
137
138    #[test]
139    fn test_parse_definition_marker_indented() {
140        assert_eq!(
141            try_parse_definition_marker("  : Definition"),
142            Some((':', 2, 1, 1))
143        );
144        assert_eq!(
145            try_parse_definition_marker("   ~ Definition"),
146            Some(('~', 3, 1, 1))
147        );
148        assert_eq!(try_parse_definition_marker("\t: Definition"), None);
149    }
150
151    #[test]
152    fn test_parse_definition_marker_too_indented() {
153        assert_eq!(try_parse_definition_marker("    : Definition"), None);
154    }
155
156    #[test]
157    fn test_parse_definition_marker_no_space_after() {
158        assert_eq!(try_parse_definition_marker(":Definition"), None);
159    }
160
161    #[test]
162    fn test_parse_definition_marker_at_eol() {
163        assert_eq!(try_parse_definition_marker(":"), Some((':', 0, 0, 0)));
164    }
165
166    #[test]
167    fn next_line_marker_ignores_colon_table_caption() {
168        let lines = vec![
169            "Here's a table with a reference:",
170            "",
171            ": (\\#tab:mytable) A table with a reference.",
172            "",
173            "| A   | B   | C   |",
174            "| --- | --- | --- |",
175            "| 1   | 2   | 3   |",
176        ];
177        assert!(is_caption_followed_by_table(&lines, 2));
178        assert_eq!(next_line_is_definition_marker(&lines, 0), None);
179    }
180
181    #[test]
182    fn test_definition_list_preserves_first_content_line_losslessly() {
183        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";
184        let tree = crate::parse(input, Some(crate::ParserOptions::default()));
185        assert_eq!(tree.text().to_string(), input);
186    }
187}