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    // Cheap byte-level leading-byte gate: a definition marker is `:` or
15    // `~` after up to 3 ASCII spaces. Avoid the `leading_indent`
16    // (Unicode char walk + tab-aware column count) on the common
17    // non-marker line.
18    {
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    // Count leading whitespace in columns (0-3 allowed)
31    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    // Check for : or ~ marker
39    let marker = after_indent.chars().next()?;
40    if !matches!(marker, ':' | '~') {
41        return None;
42    }
43
44    let after_marker = &after_indent[1..];
45
46    // Must be followed by whitespace
47    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
57/// Emit a term line into the syntax tree
58pub(crate) fn emit_term(
59    builder: &mut GreenNodeBuilder<'static>,
60    line: &str,
61    config: &ParserOptions,
62) {
63    builder.start_node(SyntaxKind::TERM.into());
64    // Strip trailing newline from line (it will be emitted separately)
65    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);
70    }
71
72    if !newline_str.is_empty() {
73        builder.token(SyntaxKind::NEWLINE.into(), newline_str);
74    }
75    builder.finish_node(); // Term
76}
77
78/// Emit a definition marker
79pub(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
90// Helper functions for definition list management in Parser
91
92use crate::parser::blocks::tables::is_caption_followed_by_table;
93use crate::parser::utils::container_stack::{Container, ContainerStack};
94
95/// Check if we're in a definition list.
96pub(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
103/// Look ahead past blank lines to find a definition marker.
104/// Returns Some(blank_line_count) if found, None otherwise.
105pub(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}