Skip to main content

panache_parser/parser/utils/
inline_emission.rs

1//! Inline element emission during block parsing.
2//!
3//! This module provides utilities for emitting inline structure directly during
4//! block parsing, using Pandoc's single-pass architecture.
5//!
6//! **Key invariant**: "Detect first, emit once"
7//! Because GreenNodeBuilder cannot backtrack, we must determine what to emit
8//! before calling builder methods. The inline parser already follows this pattern
9//! (it detects delimiters/patterns before emitting nodes).
10
11use crate::options::ParserOptions;
12use crate::parser::inlines::core;
13use rowan::GreenNodeBuilder;
14
15/// Emit inline elements from text content directly into the builder.
16///
17/// This helper calls the recursive inline parser, allowing block-level
18/// parsers to emit inline structure during parsing.
19///
20/// # Arguments
21/// * `builder` - The GreenNodeBuilder to emit nodes into
22/// * `text` - The text content to parse for inline elements
23/// * `config` - Configuration controlling which extensions are enabled
24/// * `suppress_footnote_refs` - When `true`, `[^id]` bytes are emitted as
25///   literal TEXT instead of `FOOTNOTE_REFERENCE`. Block parsers set this
26///   when the inline content lives inside a reference-style footnote
27///   definition body, mirroring pandoc's silent drop of nested refs.
28///
29/// # Example
30/// ```ignore
31/// // In a block parser (e.g., headings):
32/// builder.start_node(SyntaxKind::HEADING_CONTENT.into());
33/// emit_inlines(builder, heading_text, config, false);
34/// builder.finish_node();
35/// ```
36pub fn emit_inlines(
37    builder: &mut GreenNodeBuilder,
38    text: &str,
39    config: &ParserOptions,
40    suppress_footnote_refs: bool,
41) {
42    log::trace!(
43        "emit_inlines: {:?} ({} bytes)",
44        &text[..text.len().min(40)],
45        text.len()
46    );
47
48    // Call the recursive inline parser
49    core::parse_inline_text_recursive(builder, text, config, suppress_footnote_refs);
50}
51
52#[cfg(test)]
53mod tests {
54    use super::*;
55    use crate::options::ParserOptions;
56    use crate::syntax::{SyntaxKind, SyntaxNode};
57    use rowan::GreenNodeBuilder;
58
59    /// Test that emit_inlines produces correct inline structure.
60    #[test]
61    fn test_emit_inlines_basic() {
62        let config = ParserOptions::default();
63        let test_cases = vec![
64            "plain text",
65            "text with *emphasis*",
66            "text with **strong**",
67            "text with `code`",
68            "text with [link](url)",
69            "mixed *emph* and **strong** and `code`",
70            "nested *emphasis with `code` inside*",
71            "multiple *a* and *b* emphasis",
72        ];
73
74        for text in test_cases {
75            // Build using emit_inlines
76            let mut builder_new = GreenNodeBuilder::new();
77            builder_new.start_node(SyntaxKind::HEADING_CONTENT.into());
78            emit_inlines(&mut builder_new, text, &config, false);
79            builder_new.finish_node();
80            let green_new = builder_new.finish();
81            let tree_new = SyntaxNode::new_root(green_new);
82
83            // Verify losslessness
84            assert_eq!(
85                tree_new.text().to_string(),
86                text,
87                "Losslessness check failed for: {:?}",
88                text
89            );
90        }
91    }
92
93    /// Test that emit_inlines handles empty text correctly.
94    #[test]
95    fn test_emit_inlines_empty() {
96        let config = ParserOptions::default();
97        let mut builder = GreenNodeBuilder::new();
98        builder.start_node(SyntaxKind::HEADING_CONTENT.into());
99        emit_inlines(&mut builder, "", &config, false);
100        builder.finish_node();
101        let green = builder.finish();
102        let tree = SyntaxNode::new_root(green);
103
104        // Should produce a container with no inline content
105        assert_eq!(tree.kind(), SyntaxKind::HEADING_CONTENT);
106        assert_eq!(tree.children_with_tokens().count(), 0);
107    }
108
109    /// Test that emit_inlines preserves whitespace.
110    #[test]
111    fn test_emit_inlines_preserves_whitespace() {
112        let config = ParserOptions::default();
113        let text = "  leading and trailing  ";
114
115        let mut builder = GreenNodeBuilder::new();
116        builder.start_node(SyntaxKind::HEADING_CONTENT.into());
117        emit_inlines(&mut builder, text, &config, false);
118        builder.finish_node();
119        let green = builder.finish();
120        let tree = SyntaxNode::new_root(green);
121
122        // Should preserve all whitespace
123        assert_eq!(tree.text().to_string(), text);
124    }
125}