Skip to main content

oak_valkyrie/parser/
parse_string_segments.rs

1use crate::ast::{Expr, Identifier, Span, StringSegment};
2
3/// Fluent 变量标记字符 (Gwot, U+07DF)
4const FLUENT_MARKER: char = '\u{07DF}';
5
6/// 解析字符串内容为片段列表。
7///
8/// 该函数处理以下功能:
9/// - 插值解析:解析 `{expr}` 格式的插值表达式
10/// - 转义处理:处理 `\{` 和 `\}` 转义序列
11/// - Fluent 标记:识别 `߷` (Gwot, U+07DF) 符号标记的 Fluent 变量
12/// - Raw String 处理:前缀为 `r` 时不解析插值
13///
14/// # 参数
15///
16/// * `content` - 字符串内容(不含引号)
17/// * `span_start` - 内容在源码中的起始位置
18/// * `is_raw` - 是否为 raw string(raw string 不解析插值)
19///
20/// # 返回
21///
22/// 返回解析后的字符串片段列表
23///
24/// # 示例
25///
26/// ```
27/// use oak_valkyrie::parser::parse_string_segments;
28///
29/// // 普通文本
30/// let segments = parse_string_segments("hello", 0, false);
31/// assert_eq!(segments.len(), 1);
32///
33/// // 插值表达式
34/// let segments = parse_string_segments("hello {name}!", 0, false);
35/// assert_eq!(segments.len(), 3);
36///
37/// // Raw string 不解析插值
38/// let segments = parse_string_segments("hello {name}!", 0, true);
39/// assert_eq!(segments.len(), 1);
40/// ```
41pub fn parse_string_segments(content: &str, span_start: usize, is_raw: bool) -> Vec<StringSegment> {
42    if is_raw {
43        return vec![StringSegment::Text { content: content.to_string(), span: Span { start: span_start, end: span_start + content.len() } }];
44    }
45
46    let mut segments = Vec::new();
47    let mut current_text = String::new();
48    let mut text_start = span_start;
49    let mut chars = content.char_indices().peekable();
50    let content_len = content.len();
51
52    while let Some((idx, ch)) = chars.next() {
53        match ch {
54            '\\' => {
55                if let Some((_, next_ch)) = chars.peek() {
56                    if *next_ch == '{' || *next_ch == '}' {
57                        current_text.push(*next_ch);
58                        chars.next();
59                        continue;
60                    }
61                }
62                current_text.push(ch);
63            }
64            '{' => {
65                if !current_text.is_empty() {
66                    segments.push(StringSegment::Text { content: current_text.clone(), span: Span { start: text_start, end: span_start + idx } });
67                    current_text.clear();
68                }
69
70                let is_fluent = if let Some((_, next_ch)) = chars.peek() { *next_ch == FLUENT_MARKER } else { false };
71
72                if is_fluent {
73                    chars.next();
74                }
75
76                let expr_start = span_start + idx;
77                let mut expr_content = String::new();
78                let mut brace_count = 1;
79                let mut expr_end = span_start + idx + 1;
80
81                while let Some((inner_idx, inner_ch)) = chars.next() {
82                    match inner_ch {
83                        '{' => {
84                            brace_count += 1;
85                            expr_content.push(inner_ch);
86                        }
87                        '}' => {
88                            brace_count -= 1;
89                            if brace_count == 0 {
90                                expr_end = span_start + inner_idx;
91                                break;
92                            }
93                            expr_content.push(inner_ch);
94                        }
95                        _ => {
96                            expr_content.push(inner_ch);
97                        }
98                    }
99                }
100
101                let trimmed_expr = expr_content.trim();
102                segments.push(StringSegment::Interpolation { expr: Box::new(Expr::Ident(Identifier { name: trimmed_expr.to_string(), span: Span { start: expr_start, end: expr_end } })), is_fluent, span: Span { start: expr_start, end: expr_end + 1 } });
103
104                text_start = expr_end + 1;
105            }
106            _ => {
107                current_text.push(ch);
108            }
109        }
110    }
111
112    if !current_text.is_empty() {
113        segments.push(StringSegment::Text { content: current_text, span: Span { start: text_start, end: span_start + content_len } });
114    }
115
116    segments
117}