Skip to main content

panache_parser/parser/inlines/
raw_inline.rs

1//! Parsing for inline raw spans (`content`{=format})
2//!
3//! Raw inline spans allow embedding raw content for specific output formats.
4//! Syntax: `content`{=format}
5//! Examples:
6//! - `<a>html</a>`{=html}
7//! - `\LaTeX`{=latex}
8//! - `<w:br/>`{=openxml}
9//!
10//! This is enabled by the raw_attribute extension.
11
12use super::sink::InlineSink;
13use crate::parser::utils::attributes::{AttributeBlock, emit_attribute_node};
14use crate::syntax::SyntaxKind;
15
16/// Check if a code span with attributes is actually a raw inline span.
17/// Raw inline spans have attributes of the form {=format} (no other attributes).
18pub fn is_raw_inline(attributes: &AttributeBlock) -> Option<&str> {
19    // Raw inline must have exactly one class starting with '='
20    // and no identifier or key-value pairs
21    if attributes.identifier.is_some() || !attributes.key_values.is_empty() {
22        return None;
23    }
24
25    if attributes.classes.len() == 1 {
26        let class = &attributes.classes[0];
27        if let Some(format) = class.strip_prefix('=')
28            && !format.is_empty()
29        {
30            return Some(format);
31        }
32    }
33
34    None
35}
36
37/// Emit a raw inline span node to the builder.
38///
39/// `attr_raw` is the raw `{=format}` source slice (braces included). It is
40/// structured into `ATTR_*` children via [`emit_attribute_node`] so the node
41/// wraps the original bytes losslessly instead of synthesizing them — any
42/// interior whitespace (`{ =html }`) round-trips byte-for-byte.
43pub fn emit_raw_inline(
44    builder: &mut impl InlineSink,
45    content: &str,
46    backtick_count: usize,
47    attr_raw: &str,
48) {
49    builder.start_node(SyntaxKind::RAW_INLINE.into());
50
51    // Opening backticks
52    builder.token(
53        SyntaxKind::RAW_INLINE_MARKER.into(),
54        &"`".repeat(backtick_count),
55    );
56
57    // Raw content
58    builder.token(SyntaxKind::RAW_INLINE_CONTENT.into(), content);
59
60    // Closing backticks
61    builder.token(
62        SyntaxKind::RAW_INLINE_MARKER.into(),
63        &"`".repeat(backtick_count),
64    );
65
66    // Format attribute `{=format}`, structured over the raw source bytes.
67    emit_attribute_node(builder, attr_raw);
68
69    builder.finish_node();
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75    use crate::parser::utils::attributes::AttributeBlock;
76
77    #[test]
78    fn test_is_raw_inline_html() {
79        let attrs = AttributeBlock {
80            identifier: None,
81            classes: vec!["=html".to_string()],
82            key_values: vec![],
83        };
84        assert_eq!(is_raw_inline(&attrs), Some("html"));
85    }
86
87    #[test]
88    fn test_is_raw_inline_latex() {
89        let attrs = AttributeBlock {
90            identifier: None,
91            classes: vec!["=latex".to_string()],
92            key_values: vec![],
93        };
94        assert_eq!(is_raw_inline(&attrs), Some("latex"));
95    }
96
97    #[test]
98    fn test_is_raw_inline_openxml() {
99        let attrs = AttributeBlock {
100            identifier: None,
101            classes: vec!["=openxml".to_string()],
102            key_values: vec![],
103        };
104        assert_eq!(is_raw_inline(&attrs), Some("openxml"));
105    }
106
107    #[test]
108    fn test_not_raw_inline_regular_class() {
109        let attrs = AttributeBlock {
110            identifier: None,
111            classes: vec!["python".to_string()],
112            key_values: vec![],
113        };
114        assert_eq!(is_raw_inline(&attrs), None);
115    }
116
117    #[test]
118    fn test_not_raw_inline_with_id() {
119        let attrs = AttributeBlock {
120            identifier: Some("myid".to_string()),
121            classes: vec!["=html".to_string()],
122            key_values: vec![],
123        };
124        assert_eq!(is_raw_inline(&attrs), None);
125    }
126
127    #[test]
128    fn test_not_raw_inline_with_key_value() {
129        let attrs = AttributeBlock {
130            identifier: None,
131            classes: vec!["=html".to_string()],
132            key_values: vec![("key".to_string(), "value".to_string())],
133        };
134        assert_eq!(is_raw_inline(&attrs), None);
135    }
136
137    #[test]
138    fn test_not_raw_inline_multiple_classes() {
139        let attrs = AttributeBlock {
140            identifier: None,
141            classes: vec!["=html".to_string(), "other".to_string()],
142            key_values: vec![],
143        };
144        assert_eq!(is_raw_inline(&attrs), None);
145    }
146
147    #[test]
148    fn test_not_raw_inline_empty_format() {
149        let attrs = AttributeBlock {
150            identifier: None,
151            classes: vec!["=".to_string()],
152            key_values: vec![],
153        };
154        assert_eq!(is_raw_inline(&attrs), None);
155    }
156
157    /// The `{=format}` attribute is now structured over the raw source bytes
158    /// (an `ATTR_CLASS` token wrapping `=format`) rather than synthesized, so
159    /// the `RAW_INLINE` node round-trips byte-for-byte and exposes structure.
160    #[test]
161    fn raw_inline_attribute_is_structured_and_lossless() {
162        let input = "`<a>`{=html}\n";
163        let tree = crate::parse(input, None);
164        assert_eq!(tree.text().to_string(), input);
165
166        let attr = tree
167            .descendants()
168            .find(|n| n.kind() == SyntaxKind::ATTRIBUTE)
169            .expect("ATTRIBUTE node under RAW_INLINE");
170        assert_eq!(attr.text().to_string(), "{=html}");
171        let class = attr
172            .children_with_tokens()
173            .find(|el| el.kind() == SyntaxKind::ATTR_CLASS)
174            .and_then(|el| el.into_token())
175            .expect("ATTR_CLASS token");
176        assert_eq!(class.text(), "=html");
177    }
178
179    /// Interior whitespace inside the braces is preserved verbatim — the old
180    /// synthesizing emitter collapsed `{ =html }` to `{=html}`.
181    #[test]
182    fn raw_inline_attribute_preserves_interior_whitespace() {
183        let input = "`<a>`{ =html }\n";
184        let tree = crate::parse(input, None);
185        assert_eq!(tree.text().to_string(), input);
186    }
187}