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, false);
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            // Raw lines throughout: the marker detection above is itself raw, so
120            // this only fires outside container prefixes (at top level raw ==
121            // stripped). Inside a blockquote the raw `> :` never matches the
122            // marker, so this caption gate is unreachable there — the
123            // container-aware gate lives in `DefinitionListParser::detect_prepared`.
124            if let Some((marker, ..)) = try_parse_definition_marker(line)
125                && marker == ':'
126                && is_caption_followed_by_table(lines, check_pos)
127            {
128                return None;
129            }
130            return Some(blank_count);
131        } else {
132            return None;
133        }
134    }
135    None
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::parser::blocks::tables::is_caption_followed_by_table;
142
143    #[test]
144    fn test_parse_definition_marker_colon() {
145        assert_eq!(
146            try_parse_definition_marker(":   Definition"),
147            Some((':', 0, 3, 3))
148        );
149    }
150
151    #[test]
152    fn test_parse_definition_marker_tilde() {
153        assert_eq!(
154            try_parse_definition_marker("~   Definition"),
155            Some(('~', 0, 3, 3))
156        );
157    }
158
159    #[test]
160    fn test_parse_definition_marker_indented() {
161        assert_eq!(
162            try_parse_definition_marker("  : Definition"),
163            Some((':', 2, 1, 1))
164        );
165        assert_eq!(
166            try_parse_definition_marker("   ~ Definition"),
167            Some(('~', 3, 1, 1))
168        );
169        assert_eq!(try_parse_definition_marker("\t: Definition"), None);
170    }
171
172    #[test]
173    fn test_parse_definition_marker_too_indented() {
174        assert_eq!(try_parse_definition_marker("    : Definition"), None);
175    }
176
177    #[test]
178    fn test_parse_definition_marker_no_space_after() {
179        assert_eq!(try_parse_definition_marker(":Definition"), None);
180    }
181
182    #[test]
183    fn test_parse_definition_marker_at_eol() {
184        assert_eq!(try_parse_definition_marker(":"), Some((':', 0, 0, 0)));
185    }
186
187    #[test]
188    fn next_line_marker_ignores_colon_table_caption() {
189        let lines = vec![
190            "Here's a table with a reference:",
191            "",
192            ": (\\#tab:mytable) A table with a reference.",
193            "",
194            "| A   | B   | C   |",
195            "| --- | --- | --- |",
196            "| 1   | 2   | 3   |",
197        ];
198        assert!(is_caption_followed_by_table(&lines[..], 2));
199        assert_eq!(next_line_is_definition_marker(&lines, 0), None);
200    }
201
202    #[test]
203    fn test_definition_list_preserves_first_content_line_losslessly() {
204        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";
205        let tree = crate::parse(input, Some(crate::ParserOptions::default()));
206        assert_eq!(tree.text().to_string(), input);
207    }
208}