Skip to main content

panache_parser/parser/blocks/
definition_lists.rs

1use crate::config::Config;
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(builder: &mut GreenNodeBuilder<'static>, line: &str, config: &Config) {
43    builder.start_node(SyntaxKind::TERM.into());
44    // Strip trailing newline from line (it will be emitted separately)
45    let (text, newline_str) = strip_newline(line);
46    let trimmed_text = text.trim_end();
47
48    if !trimmed_text.is_empty() {
49        inline_emission::emit_inlines(builder, trimmed_text, config);
50    }
51
52    if !newline_str.is_empty() {
53        builder.token(SyntaxKind::NEWLINE.into(), newline_str);
54    }
55    builder.finish_node(); // Term
56}
57
58/// Emit a definition marker
59pub(crate) fn emit_definition_marker(
60    builder: &mut GreenNodeBuilder<'static>,
61    marker: char,
62    indent_cols: usize,
63) {
64    if indent_cols > 0 {
65        builder.token(SyntaxKind::WHITESPACE.into(), &" ".repeat(indent_cols));
66    }
67    builder.token(SyntaxKind::DEFINITION_MARKER.into(), &marker.to_string());
68}
69
70// Helper functions for definition list management in Parser
71
72use crate::parser::utils::container_stack::{Container, ContainerStack};
73
74/// Check if we're in a definition list.
75pub(in crate::parser) fn in_definition_list(containers: &ContainerStack) -> bool {
76    containers
77        .stack
78        .iter()
79        .any(|c| matches!(c, Container::DefinitionList { .. }))
80}
81
82/// Look ahead past blank lines to find a definition marker.
83/// Returns Some(blank_line_count) if found, None otherwise.
84pub(in crate::parser) fn next_line_is_definition_marker(
85    lines: &[&str],
86    pos: usize,
87) -> Option<usize> {
88    let mut check_pos = pos + 1;
89    let mut blank_count = 0;
90    while check_pos < lines.len() {
91        let line = lines[check_pos];
92        if line.trim().is_empty() {
93            blank_count += 1;
94            check_pos += 1;
95            continue;
96        }
97        if try_parse_definition_marker(line).is_some() {
98            return Some(blank_count);
99        } else {
100            return None;
101        }
102    }
103    None
104}
105
106#[cfg(test)]
107mod tests {
108    use super::*;
109
110    #[test]
111    fn test_parse_definition_marker_colon() {
112        assert_eq!(
113            try_parse_definition_marker(":   Definition"),
114            Some((':', 0, 3, 3))
115        );
116    }
117
118    #[test]
119    fn test_parse_definition_marker_tilde() {
120        assert_eq!(
121            try_parse_definition_marker("~   Definition"),
122            Some(('~', 0, 3, 3))
123        );
124    }
125
126    #[test]
127    fn test_parse_definition_marker_indented() {
128        assert_eq!(
129            try_parse_definition_marker("  : Definition"),
130            Some((':', 2, 1, 1))
131        );
132        assert_eq!(
133            try_parse_definition_marker("   ~ Definition"),
134            Some(('~', 3, 1, 1))
135        );
136        assert_eq!(try_parse_definition_marker("\t: Definition"), None);
137    }
138
139    #[test]
140    fn test_parse_definition_marker_too_indented() {
141        assert_eq!(try_parse_definition_marker("    : Definition"), None);
142    }
143
144    #[test]
145    fn test_parse_definition_marker_no_space_after() {
146        assert_eq!(try_parse_definition_marker(":Definition"), None);
147    }
148
149    #[test]
150    fn test_parse_definition_marker_at_eol() {
151        assert_eq!(try_parse_definition_marker(":"), Some((':', 0, 0, 0)));
152    }
153
154    #[test]
155    fn test_definition_list_preserves_first_content_line_losslessly() {
156        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";
157        let tree = crate::parse(input, Some(crate::Config::default()));
158        assert_eq!(tree.text().to_string(), input);
159    }
160}