Skip to main content

sz_rust_mvc_facade/view/
template.rs

1//! Cx 标签库控制流标签 — 对齐 PHP `think\template\taglib\Cx`
2//!
3//! 实现对齐 PHP Cx 标签库的控制流标签解析与渲染。
4//!
5//! ## 支持的标签
6//!
7//! | 标签 | PHP 对齐方法 | 说明 |
8//! |------|-------------|------|
9//! | `{if}/{elseif}/{else}` | `tagIf/tagElseif/tagElse` | 条件判断 |
10//! | `{foreach}` | `tagForeach` | foreach 循环 |
11//! | `{volist}` | `tagVolist` | volist 循环 |
12//! | `{switch}/{case}/{default}` | `tagSwitch/tagCase/tagDefault` | switch 分支 |
13//! | `{for}` | `tagFor` | for 循环 |
14//!
15//! ## PHP 对齐说明
16//!
17//! ### 条件简写(对齐 PHP `TagLib::parseCondition`)
18//! 使用 `str_ireplace`(大小写不敏感)替换:
19//! - ` eq ` → ` == `
20//! - ` neq ` → ` != `
21//! - ` gt ` → ` > `
22//! - ` lt ` → ` < `
23//! - ` egt ` → ` >= `
24//! - ` elt ` → ` <= `
25//! - ` heq ` → ` === `
26//! - ` nheq ` → ` !== `
27//!
28//! 简写两侧必须有空格(对齐 PHP `$comparison` 数组的 key 格式)。
29//!
30//! ### autoBuildVar(对齐 PHP `TagLib::autoBuildVar`)
31//! - `:` 开头 → 函数调用,去掉 `:` 前缀
32//! - `$` 开头 → 变量引用,保留
33//! - 字母/下划线开头 → 自动补 `$` 前缀(视为变量名)
34//! - 其他 → 字面量
35//!
36//! ### volist 的 key 属性(对齐 PHP `tagVolist`)
37//! `key` 属性是计数器变量名(默认 `i`,1-based),不是数组键变量名。
38//! 数组键总是赋给字面变量 `$key`。
39//!
40//! ### case 的 break 陷阱(对齐 PHP `tagCase`)
41//! `break="false"` 中字符串 `"false"` 是 truthy,实际会 break。
42//! 要实现贯穿需用 `break="0"`(字符串 `"0"` 是 falsy)。
43//!
44//! ## PHP 源码参考
45//! - `think-template\src\template\taglib\Cx.php`(715 行)
46//! - `think-template\src\template\TagLib.php`(349 行)
47
48use std::collections::HashMap;
49
50use regex::Regex;
51use serde_json::Value;
52
53#[cfg(test)]
54use super::value_to_string;
55use super::{is_truthy, parse_literal, resolve_var_expr, ViewConfig, ViewData, ViewError};
56
57// ============================================================================
58// 公共入口
59// ============================================================================
60
61/// 渲染控制流标签(对齐 PHP `TagLib::parseTag`)
62///
63/// 在 `render_content` 中调用,处理 `{if}`、`{foreach}`、`{volist}`、`{switch}`、`{for}` 块标签。
64/// 对每个块标签的内部内容递归调用 `render_inner`(即 `render_content`),以处理嵌套控制流和变量标签。
65///
66/// # 参数
67/// - `content` — 模板内容
68/// - `data` — 模板变量
69/// - `config` — 视图配置
70/// - `render_inner` — 内部内容渲染器(递归调用 `render_content`)
71pub fn render_control_flow<F>(
72    content: &str,
73    data: &ViewData,
74    config: &ViewConfig,
75    render_inner: F,
76) -> Result<String, ViewError>
77where
78    F: Fn(&str, &ViewData) -> Result<String, ViewError>,
79{
80    let renderer = CxRenderer {
81        config,
82        render_inner,
83    };
84    renderer.render(content, data)
85}
86
87// ============================================================================
88// CxRenderer — 控制流标签渲染器
89// ============================================================================
90
91/// Cx 标签渲染器
92struct CxRenderer<'a, F>
93where
94    F: Fn(&str, &ViewData) -> Result<String, ViewError>,
95{
96    config: &'a ViewConfig,
97    render_inner: F,
98}
99
100impl<'a, F> CxRenderer<'a, F>
101where
102    F: Fn(&str, &ViewData) -> Result<String, ViewError>,
103{
104    /// 渲染主循环:扫描控制流开标签,找到匹配闭标签,渲染块内容
105    fn render(&self, content: &str, data: &ViewData) -> Result<String, ViewError> {
106        let begin = &self.config.tpl_begin;
107        let begin_esc = regex::escape(begin);
108
109        // 匹配 {if / {foreach / {volist / {switch / {for(后跟空白或 `(`)
110        let pattern = format!(r"{}\s*(if|foreach|volist|switch|for)(\s|\()", begin_esc);
111        let re = Regex::new(&pattern).map_err(|e| ViewError::SyntaxError(e.to_string()))?;
112
113        let mut result = String::with_capacity(content.len());
114        let mut last_end = 0;
115
116        for caps in re.captures_iter(content) {
117            let m = caps
118                .get(0)
119                .ok_or_else(|| ViewError::SyntaxError("正则捕获组 0(整体匹配)缺失".into()))?;
120            // 跳过已处理区域内的匹配(嵌套标签已由 render_inner 递归处理)
121            if m.start() < last_end {
122                continue;
123            }
124            let tag_type = caps
125                .get(1)
126                .ok_or_else(|| ViewError::SyntaxError("正则捕获组 1(标签类型)缺失".into()))?
127                .as_str();
128            let separator = caps
129                .get(2)
130                .ok_or_else(|| ViewError::SyntaxError("正则捕获组 2(分隔符)缺失".into()))?
131                .as_str(); // 空格或 `(`
132
133            // 找到开标签的闭合 `}`
134            let after_name = m.start() + m.len() - separator.len();
135            let end_str = &self.config.tpl_end;
136            let tag_end_pos = match content[after_name..].find(end_str.as_str()) {
137                Some(pos) => after_name + pos + end_str.len(),
138                None => continue, // 无闭合 `}`,跳过
139            };
140
141            result.push_str(&content[last_end..m.start()]);
142
143            // 提取开标签属性内容({tag_name ... } 中的 `...`)
144            let tag_content_start = m.start() + begin.len() + tag_type.len();
145            // tag_content_start 指向 separator(空格或 `(`)
146            let tag_inner = &content[tag_content_start..tag_end_pos - end_str.len()];
147
148            // 找到匹配的闭标签
149            match self.find_matching_close(content, tag_end_pos, tag_type) {
150                Ok((block_content, block_end)) => {
151                    let rendered = match tag_type {
152                        "if" => self.render_if(tag_inner, &block_content, data)?,
153                        "foreach" => self.render_foreach(tag_inner, &block_content, data)?,
154                        "volist" => self.render_volist(tag_inner, &block_content, data)?,
155                        "switch" => self.render_switch(tag_inner, &block_content, data)?,
156                        "for" => self.render_for(tag_inner, &block_content, data)?,
157                        _ => block_content.to_string(),
158                    };
159                    result.push_str(&rendered);
160                    last_end = block_end;
161                }
162                Err(e) => {
163                    // 未闭合标签:输出错误提示
164                    return Err(e);
165                }
166            }
167        }
168
169        result.push_str(&content[last_end..]);
170        Ok(result)
171    }
172
173    /// 找到匹配的闭标签(对齐 PHP `TagLib::parseTag` 栈式匹配)
174    ///
175    /// 从 `start` 位置开始,查找与 `tag_type` 匹配的闭标签 `{/tag_type}`。
176    /// 跟踪嵌套深度,正确处理同名标签的嵌套。
177    fn find_matching_close(
178        &self,
179        content: &str,
180        start: usize,
181        tag_type: &str,
182    ) -> Result<(String, usize), ViewError> {
183        let begin = &self.config.tpl_begin;
184        let end = &self.config.tpl_end;
185
186        let open_prefix = format!("{}{}", begin, tag_type);
187        let close_tag = format!("{}{}{}{}", begin, "/", tag_type, end);
188
189        let mut depth: i32 = 1;
190        let mut pos = start;
191
192        while pos < content.len() {
193            let remaining = &content[pos..];
194
195            // 查找下一个闭标签
196            let next_close = remaining.find(&close_tag);
197
198            // 查找下一个开标签(必须后跟空白或 `(` 或 `}`)
199            let next_open = remaining.find(&open_prefix).and_then(|p| {
200                let after = pos + p + open_prefix.len();
201                if after < content.len() {
202                    let next_char = content.as_bytes()[after];
203                    // 开标签后必须是空白、`(` 或 `}`
204                    if next_char == b' '
205                        || next_char == b'\t'
206                        || next_char == b'\n'
207                        || next_char == b'\r'
208                        || next_char == b'('
209                        || content[after..].starts_with(end.as_str())
210                    {
211                        return Some(p);
212                    }
213                }
214                None
215            });
216
217            match (next_open, next_close) {
218                (_, None) => {
219                    return Err(ViewError::SyntaxError(format!(
220                        "未闭合的 {{{}}} 标签",
221                        tag_type
222                    )));
223                }
224                (Some(open_pos), Some(close_pos)) if open_pos < close_pos => {
225                    depth += 1;
226                    pos += open_pos + open_prefix.len();
227                }
228                (_, Some(close_pos)) => {
229                    depth -= 1;
230                    if depth == 0 {
231                        let block_content = content[start..pos + close_pos].to_string();
232                        let block_end = pos + close_pos + close_tag.len();
233                        return Ok((block_content, block_end));
234                    }
235                    pos += close_pos + close_tag.len();
236                }
237            }
238        }
239
240        Err(ViewError::SyntaxError(format!(
241            "未闭合的 {{{}}} 标签",
242            tag_type
243        )))
244    }
245
246    // ========================================================================
247    // {if}/{elseif}/{else} — 对齐 PHP `tagIf/tagElseif/tagElse`
248    // ========================================================================
249
250    /// 渲染 {if}...{/if} 块
251    ///
252    /// PHP `tagIf` 编译为 `<?php if(...): ?>...<?php endif; ?>`。
253    /// Rust 改为直接求值条件,渲染匹配分支的内容。
254    ///
255    /// 支持两种语法:
256    /// - 属性形式:`{if condition="$a eq 1"}`
257    /// - 表达式形式:`{if ($a == 1)}`
258    fn render_if(
259        &self,
260        tag_inner: &str,
261        block_content: &str,
262        data: &ViewData,
263    ) -> Result<String, ViewError> {
264        let attrs = parse_attributes(tag_inner);
265        let condition = if let Some(cond) = attrs.get("condition") {
266            cond.clone()
267        } else {
268            // 表达式形式:tag_inner 本身是条件表达式
269            // 去掉前导空白和前导 `(`
270            tag_inner
271                .trim()
272                .trim_start_matches('(')
273                .trim_end_matches(')')
274                .trim()
275                .to_string()
276        };
277
278        // 求值 if 条件
279        if evaluate_condition(&condition, data)? {
280            // 渲染 if 分支内容(在 `{elseif}` 或 `{else}` 之前的部分)
281            let if_body = self.split_if_branches(block_content).0;
282            return (self.render_inner)(&if_body, data);
283        }
284
285        // 查找 {elseif} 和 {else} 分支
286        let branches = self.split_if_branches(block_content);
287        let branches_ref = &branches.1;
288
289        for (elseif_cond, elseif_body) in branches_ref {
290            if evaluate_condition(elseif_cond, data)? {
291                return (self.render_inner)(elseif_body, data);
292            }
293        }
294
295        // {else} 分支
296        if let Some(else_body) = &branches.2 {
297            return (self.render_inner)(else_body, data);
298        }
299
300        Ok(String::new())
301    }
302
303    /// 将 {if} 块内容拆分为 (if_body, elseif_branches, else_body)
304    ///
305    /// `elseif_branches` 是 `Vec<(condition, body)>`
306    /// `else_body` 是 `Option<String>`
307    fn split_if_branches(&self, content: &str) -> (String, Vec<(String, String)>, Option<String>) {
308        let begin = &self.config.tpl_begin;
309        let end = &self.config.tpl_end;
310
311        // 匹配 {elseif ... /} 或 {elseif ...}
312        // 注意:format! 中字面 `{}` 必须用 `{{}}` 转义;去掉 r 前缀并将 `\s` 写成 `\\s`
313        let elseif_pattern = format!(
314            "{}\\s*elseif\\s+([^{{}}]*?)(?:\\s*/)?{}",
315            regex::escape(begin),
316            regex::escape(end)
317        );
318        // 匹配 {else /} 或 {else}
319        let else_pattern = format!(
320            r"{}\s*else\s*/?{}",
321            regex::escape(begin),
322            regex::escape(end)
323        );
324
325        let elseif_re = Regex::new(&elseif_pattern).unwrap_or_else(|e| panic!("elseif regex: {e}"));
326        let else_re = Regex::new(&else_pattern).unwrap_or_else(|e| panic!("else regex: {e}"));
327
328        // 找到第一个 {elseif} 或 {else} 的位置
329        let first_elseif = elseif_re.find(content).map(|m| m.start());
330        let first_else = else_re.find(content).map(|m| m.start());
331
332        let first_split = match (first_elseif, first_else) {
333            (Some(ei), Some(el)) => ei.min(el),
334            (Some(ei), None) => ei,
335            (None, Some(el)) => el,
336            (None, None) => return (content.to_string(), Vec::new(), None),
337        };
338
339        let if_body = content[..first_split].to_string();
340        let rest = &content[first_split..];
341
342        let mut elseif_branches = Vec::new();
343        let mut else_body = None;
344        let mut remaining = rest;
345
346        loop {
347            // 尝试匹配 {elseif condition="..." /}
348            if let Some(m) = elseif_re.find(remaining) {
349                if m.start() == 0 {
350                    let attrs_str = elseif_re
351                        .captures(remaining)
352                        .expect("elseif_re 已通过 find 匹配且 start==0,captures 必成功")
353                        .get(1)
354                        .expect("elseif 正则含捕获组 1,匹配成功时必存在")
355                        .as_str();
356                    let attrs = parse_attributes(attrs_str);
357                    let cond = attrs
358                        .get("condition")
359                        .cloned()
360                        .unwrap_or_else(|| attrs_str.trim().to_string());
361                    let after = &remaining[m.end()..];
362
363                    // 找到下一个 {elseif} 或 {else} 的位置
364                    let next_split = self.find_next_branch(after);
365
366                    let body = match next_split {
367                        Some(pos) => {
368                            let b = after[..pos].to_string();
369                            remaining = &after[pos..];
370                            b
371                        }
372                        None => {
373                            let b = after.to_string();
374                            remaining = "";
375                            b
376                        }
377                    };
378                    elseif_branches.push((cond, body));
379                    if remaining.is_empty() {
380                        break;
381                    }
382                    continue;
383                }
384            }
385
386            // 尝试匹配 {else /}
387            if let Some(m) = else_re.find(remaining) {
388                if m.start() == 0 {
389                    else_body = Some(remaining[m.end()..].to_string());
390                    break;
391                }
392            }
393
394            break;
395        }
396
397        (if_body, elseif_branches, else_body)
398    }
399
400    /// 在内容中查找下一个 {elseif} 或 {else} 标签的位置
401    fn find_next_branch(&self, content: &str) -> Option<usize> {
402        let begin = &self.config.tpl_begin;
403        let end = &self.config.tpl_end;
404
405        let elseif_pattern = format!(
406            "{}\\s*elseif\\s+[^{{}}]*?{}",
407            regex::escape(begin),
408            regex::escape(end)
409        );
410        let else_pattern = format!(
411            "{}\\s*else\\s*/?{}",
412            regex::escape(begin),
413            regex::escape(end)
414        );
415
416        let elseif_re = Regex::new(&elseif_pattern).ok()?;
417        let else_re = Regex::new(&else_pattern).ok()?;
418
419        let ei = elseif_re.find(content).map(|m| m.start());
420        let el = else_re.find(content).map(|m| m.start());
421
422        match (ei, el) {
423            (Some(a), Some(b)) => Some(a.min(b)),
424            (Some(a), None) => Some(a),
425            (None, Some(b)) => Some(b),
426            (None, None) => None,
427        }
428    }
429
430    // ========================================================================
431    // {foreach} — 对齐 PHP `tagForeach`
432    // ========================================================================
433
434    /// 渲染 {foreach}...{/foreach} 块
435    ///
436    /// PHP `tagForeach` 支持两种形式:
437    /// - 属性形式:`{foreach name="list" id="item" key="k" index="i"}`
438    /// - 表达式形式:`{foreach $list as $k=>$v}`
439    ///
440    /// 属性说明:
441    /// - `name` — 数组变量名(不含 `$`)
442    /// - `id` / `item` — 元素变量名
443    /// - `key` — 键变量名(默认 `key`)
444    /// - `index` — 索引计数器变量名(默认 `i`,0-based)
445    /// - `offset` — 起始偏移
446    /// - `length` — 最大长度
447    /// - `empty` — 空数据提示
448    fn render_foreach(
449        &self,
450        tag_inner: &str,
451        block_content: &str,
452        data: &ViewData,
453    ) -> Result<String, ViewError> {
454        let attrs = parse_attributes(tag_inner);
455
456        // 表达式形式:{foreach ($list as $k=>$v)}
457        // PHP `parseAttr` 在无 key="value" 属性且标签支持 expression 时,将整个标签内容作为 expression
458        if attrs.is_empty() && tag_inner.trim().starts_with('(') {
459            return self.render_foreach_expression(tag_inner.trim(), block_content, data);
460        }
461        if let Some(expr) = attrs.get("expression") {
462            return self.render_foreach_expression(expr, block_content, data);
463        }
464
465        let name = attrs.get("name").cloned().unwrap_or_default();
466        let item_name = attrs
467            .get("id")
468            .or_else(|| attrs.get("item"))
469            .cloned()
470            .unwrap_or_else(|| "item".to_string());
471        let key_name = attrs
472            .get("key")
473            .cloned()
474            .unwrap_or_else(|| "key".to_string());
475        let index_name = attrs.get("index").cloned();
476        let empty_msg = attrs.get("empty").cloned().unwrap_or_default();
477        let offset: usize = attrs
478            .get("offset")
479            .and_then(|v| v.parse().ok())
480            .unwrap_or(0);
481        let length: Option<usize> = attrs.get("length").and_then(|v| v.parse().ok());
482
483        // 解析数组变量
484        let array_val = resolve_name(&name, data);
485
486        let items = match &array_val {
487            Value::Array(arr) => arr.clone(),
488            Value::Object(map) => {
489                // 对象按 key=>value 遍历
490                map.iter()
491                    .map(|(k, v)| serde_json::json!({"key": k, "value": v.clone()}))
492                    .collect()
493            }
494            _ => Vec::new(),
495        };
496
497        // 应用 offset 和 length
498        let start = offset.min(items.len());
499        let end = match length {
500            Some(len) => (start + len).min(items.len()),
501            None => items.len(),
502        };
503        let slice = &items[start..end];
504
505        if slice.is_empty() && !empty_msg.is_empty() {
506            return Ok(empty_msg);
507        }
508
509        let mut result = String::new();
510        for (idx, item) in slice.iter().enumerate() {
511            let mut child_data = data.clone();
512
513            // 设置元素变量
514            if array_val.is_array() {
515                child_data.insert(item_name.clone(), item.clone());
516            } else {
517                // 对象遍历:从 {key, value} 对中提取
518                if let Some(v) = item.get("value") {
519                    child_data.insert(item_name.clone(), v.clone());
520                }
521                if let Some(k) = item.get("key").and_then(|k| k.as_str()) {
522                    child_data.insert(key_name.clone(), Value::String(k.to_string()));
523                }
524            }
525
526            // 数组遍历:设置 key
527            if array_val.is_array() {
528                if let Value::Array(arr) = &array_val {
529                    let actual_idx = start + idx;
530                    if let Some(actual_item) = arr.get(actual_idx) {
531                        child_data.insert(item_name.clone(), actual_item.clone());
532                    }
533                    child_data.insert(key_name.clone(), Value::Number((actual_idx).into()));
534                }
535            }
536
537            // 设置索引计数器
538            if let Some(ref index) = index_name {
539                child_data.insert(index.clone(), Value::Number((idx).into()));
540            }
541
542            let rendered = (self.render_inner)(block_content, &child_data)?;
543            result.push_str(&rendered);
544        }
545
546        Ok(result)
547    }
548
549    /// 渲染表达式形式 {foreach $list as $k=>$v}
550    fn render_foreach_expression(
551        &self,
552        expr: &str,
553        block_content: &str,
554        data: &ViewData,
555    ) -> Result<String, ViewError> {
556        // 去掉括号
557        let expr = expr
558            .trim()
559            .trim_start_matches('(')
560            .trim_end_matches(')')
561            .trim();
562
563        // 解析 `$list as $k=>$v` 或 `$list as $v`
564        let as_pos = expr
565            .find(" as ")
566            .ok_or_else(|| ViewError::SyntaxError("foreach 表达式缺少 'as'".to_string()))?;
567
568        let name_part = expr[..as_pos].trim();
569        let after_as = expr[as_pos + 4..].trim();
570
571        // 去掉变量前缀 $
572        let name = name_part.trim_start_matches('$');
573        let array_val = resolve_var_expr(name, data);
574
575        let (key_name, item_name) = if let Some(arrow_pos) = after_as.find("=>") {
576            let k = after_as[..arrow_pos].trim().trim_start_matches('$');
577            let v = after_as[arrow_pos + 2..].trim().trim_start_matches('$');
578            (k.to_string(), v.to_string())
579        } else {
580            (
581                "key".to_string(),
582                after_as.trim_start_matches('$').to_string(),
583            )
584        };
585
586        let mut result = String::new();
587
588        match &array_val {
589            Value::Array(arr) => {
590                for (idx, item) in arr.iter().enumerate() {
591                    let mut child_data = data.clone();
592                    child_data.insert(item_name.clone(), item.clone());
593                    child_data.insert(key_name.clone(), Value::Number((idx).into()));
594                    let rendered = (self.render_inner)(block_content, &child_data)?;
595                    result.push_str(&rendered);
596                }
597            }
598            Value::Object(map) => {
599                for (k, v) in map.iter() {
600                    let mut child_data = data.clone();
601                    child_data.insert(item_name.clone(), v.clone());
602                    child_data.insert(key_name.clone(), Value::String(k.clone()));
603                    let rendered = (self.render_inner)(block_content, &child_data)?;
604                    result.push_str(&rendered);
605                }
606            }
607            _ => {}
608        }
609
610        Ok(result)
611    }
612
613    // ========================================================================
614    // {volist} — 对齐 PHP `tagVolist`
615    // ========================================================================
616
617    /// 渲染 {volist}...{/volist} 块
618    ///
619    /// PHP `tagVolist` 与 `tagForeach` 的关键差异:
620    /// - `key` 属性是计数器变量名(默认 `i`,1-based),不是数组键变量名
621    /// - 数组键总是赋给字面变量 `$key`
622    /// - 不支持表达式形式
623    ///
624    /// 属性说明:
625    /// - `name` — 数组变量名
626    /// - `id` — 元素变量名
627    /// - `key` — 计数器变量名(默认 `i`,1-based)
628    /// - `offset` — 起始偏移
629    /// - `length` — 最大长度
630    /// - `mod` — 取模值(默认 2)
631    /// - `empty` — 空数据提示
632    fn render_volist(
633        &self,
634        tag_inner: &str,
635        block_content: &str,
636        data: &ViewData,
637    ) -> Result<String, ViewError> {
638        let attrs = parse_attributes(tag_inner);
639
640        let name = attrs.get("name").cloned().unwrap_or_default();
641        let id_name = attrs
642            .get("id")
643            .cloned()
644            .unwrap_or_else(|| "item".to_string());
645        let key_name = attrs.get("key").cloned().unwrap_or_else(|| "i".to_string());
646        let empty_msg = attrs.get("empty").cloned().unwrap_or_default();
647        let offset: usize = attrs
648            .get("offset")
649            .and_then(|v| v.parse().ok())
650            .unwrap_or(0);
651        let length: Option<usize> = attrs.get("length").and_then(|v| v.parse().ok());
652
653        // 解析数组变量
654        let array_val = resolve_name(&name, data);
655
656        // 收集 (key, value) 对
657        let pairs: Vec<(Value, Value)> = match &array_val {
658            Value::Array(arr) => arr
659                .iter()
660                .enumerate()
661                .map(|(i, v)| (Value::Number(i.into()), v.clone()))
662                .collect(),
663            Value::Object(map) => map
664                .iter()
665                .map(|(k, v)| (Value::String(k.clone()), v.clone()))
666                .collect(),
667            _ => Vec::new(),
668        };
669
670        // 应用 offset 和 length
671        let start = offset.min(pairs.len());
672        let end = match length {
673            Some(len) => (start + len).min(pairs.len()),
674            None => pairs.len(),
675        };
676
677        if start >= end && !empty_msg.is_empty() {
678            return Ok(empty_msg);
679        }
680
681        let mut result = String::new();
682        for (view_idx, (key_val, item_val)) in pairs[start..end].iter().enumerate() {
683            let mut child_data = data.clone();
684
685            // PHP volist: $key = 数组原始键,$i (key属性) = 1-based 计数器
686            child_data.insert("key".to_string(), key_val.clone());
687            child_data.insert(key_name.clone(), Value::Number((view_idx + 1).into()));
688            child_data.insert(id_name.clone(), item_val.clone());
689
690            let rendered = (self.render_inner)(block_content, &child_data)?;
691            result.push_str(&rendered);
692        }
693
694        Ok(result)
695    }
696
697    // ========================================================================
698    // {switch}/{case}/{default} — 对齐 PHP `tagSwitch/tagCase/tagDefault`
699    // ========================================================================
700
701    /// 渲染 {switch}...{/switch} 块
702    ///
703    /// PHP `tagSwitch` 编译为 `<?php switch(...): ?>...<?php endswitch; ?>`。
704    ///
705    /// {case} 标签支持:
706    /// - `value="1"` — 单值匹配
707    /// - `value="1|2|3"` — 多值匹配(`|` 分隔)
708    /// - `break="false"` — 陷阱:字符串 "false" 是 truthy,实际会 break
709    /// - `break="0"` — 贯穿(字符串 "0" 是 falsy)
710    fn render_switch(
711        &self,
712        tag_inner: &str,
713        block_content: &str,
714        data: &ViewData,
715    ) -> Result<String, ViewError> {
716        let attrs = parse_attributes(tag_inner);
717        let name = attrs.get("name").cloned().unwrap_or_default();
718        let switch_val = resolve_name(&name, data);
719
720        // 解析 case 和 default 分支
721        let cases = self.parse_switch_cases(block_content);
722
723        let mut matched = false;
724        let mut result = String::new();
725        let mut fall_through = false;
726
727        for case in &cases {
728            if fall_through {
729                // 贯穿:渲染当前 case 内容
730                let rendered = (self.render_inner)(&case.content, data)?;
731                result.push_str(&rendered);
732                if case.should_break() {
733                    break;
734                }
735                continue;
736            }
737
738            if matched {
739                break;
740            }
741
742            match &case.kind {
743                CaseKind::Default => {
744                    matched = true;
745                    let rendered = (self.render_inner)(&case.content, data)?;
746                    result.push_str(&rendered);
747                    if case.should_break() {
748                        break;
749                    }
750                    fall_through = true;
751                }
752                CaseKind::Values(values) => {
753                    for val in values {
754                        let case_val = resolve_case_value(val, data);
755                        if loose_equal(&switch_val, &case_val) {
756                            matched = true;
757                            let rendered = (self.render_inner)(&case.content, data)?;
758                            result.push_str(&rendered);
759                            if case.should_break() {
760                                break;
761                            }
762                            fall_through = true;
763                            break;
764                        }
765                    }
766                }
767            }
768        }
769
770        Ok(result)
771    }
772
773    /// 解析 {switch} 块内容中的 {case} 和 {default} 分支
774    fn parse_switch_cases(&self, content: &str) -> Vec<SwitchCase> {
775        let begin = &self.config.tpl_begin;
776        let end = &self.config.tpl_end;
777
778        // 匹配 {case value="..." /} 或 {case value="..."}
779        let case_pattern = format!(
780            "{}\\s*case\\s+([^{{}}]*?)(?:\\s*/)?{}",
781            regex::escape(begin),
782            regex::escape(end)
783        );
784        // 匹配 {default /} 或 {default}
785        let default_pattern = format!(
786            "{}\\s*default\\s*/?{}",
787            regex::escape(begin),
788            regex::escape(end)
789        );
790
791        let case_re = Regex::new(&case_pattern).unwrap_or_else(|e| panic!("case regex: {e}"));
792        let default_re =
793            Regex::new(&default_pattern).unwrap_or_else(|e| panic!("default regex: {e}"));
794
795        let mut cases = Vec::new();
796        let mut pos = 0;
797
798        while pos < content.len() {
799            // 查找下一个 {case} 或 {default}
800            let next_case = case_re
801                .find(&content[pos..])
802                .map(|m| (pos + m.start(), m.end() - m.start()));
803            let next_default = default_re
804                .find(&content[pos..])
805                .map(|m| (pos + m.start(), m.end() - m.start()));
806
807            let (tag_start, tag_len, is_default) = match (next_case, next_default) {
808                (Some((cs, cl)), Some((ds, dl))) => {
809                    if cs <= ds {
810                        (cs, cl, false)
811                    } else {
812                        (ds, dl, true)
813                    }
814                }
815                (Some((cs, cl)), None) => (cs, cl, false),
816                (None, Some((ds, dl))) => (ds, dl, true),
817                (None, None) => break,
818            };
819
820            // 提取标签属性
821            let tag_str = &content[tag_start..tag_start + tag_len];
822
823            if is_default {
824                let body_start = tag_start + tag_len;
825                // 查找下一个 {case} 或 {default} 或内容结束
826                let (body_end, is_close_case) = self
827                    .find_next_case_or_default(&content[body_start..])
828                    .map(|(p, is_cc)| (body_start + p, is_cc))
829                    .unwrap_or((content.len(), false));
830                let body = content[body_start..body_end].to_string();
831                cases.push(SwitchCase {
832                    kind: CaseKind::Default,
833                    content: body,
834                    break_attr: None,
835                });
836                // 如果遇到 {/case},跳过它
837                let close_case_tag = format!("{}/case{}", begin, end);
838                if is_close_case {
839                    pos = body_end + close_case_tag.len();
840                } else {
841                    pos = body_end;
842                }
843            } else {
844                let caps = case_re
845                    .captures(tag_str)
846                    .expect("else 分支仅处理 case 标签,case_re 必匹配");
847                let attrs_str = caps
848                    .get(1)
849                    .expect("case 正则含捕获组 1,匹配成功时必存在")
850                    .as_str();
851                let attrs = parse_attributes(attrs_str);
852                let value = attrs.get("value").cloned().unwrap_or_default();
853                let break_attr = attrs.get("break").cloned();
854
855                let body_start = tag_start + tag_len;
856                let (body_end, is_close_case) = self
857                    .find_next_case_or_default(&content[body_start..])
858                    .map(|(p, is_cc)| (body_start + p, is_cc))
859                    .unwrap_or((content.len(), false));
860                let body = content[body_start..body_end].to_string();
861
862                // 解析 value(支持 | 多值)
863                let values: Vec<String> = value.split('|').map(|s| s.trim().to_string()).collect();
864
865                cases.push(SwitchCase {
866                    kind: CaseKind::Values(values),
867                    content: body,
868                    break_attr,
869                });
870                // 如果遇到 {/case},跳过它
871                let close_case_tag = format!("{}/case{}", begin, end);
872                if is_close_case {
873                    pos = body_end + close_case_tag.len();
874                } else {
875                    pos = body_end;
876                }
877            }
878        }
879
880        cases
881    }
882
883    /// 查找下一个 {case}、{default} 或 {/case} 标签位置
884    ///
885    /// 返回 `(位置, 是否是 {/case} 闭合标签)`
886    fn find_next_case_or_default(&self, content: &str) -> Option<(usize, bool)> {
887        let begin = &self.config.tpl_begin;
888        let end = &self.config.tpl_end;
889
890        let case_pattern = format!(
891            "{}\\s*case\\s+[^{{}}]*?{}",
892            regex::escape(begin),
893            regex::escape(end)
894        );
895        let default_pattern = format!(
896            "{}\\s*default\\s*/?{}",
897            regex::escape(begin),
898            regex::escape(end)
899        );
900        // {/case} 闭合标签
901        let close_case_pattern = format!(
902            "{}\\s*/case\\s*{}",
903            regex::escape(begin),
904            regex::escape(end)
905        );
906
907        let case_re = Regex::new(&case_pattern).ok()?;
908        let default_re = Regex::new(&default_pattern).ok()?;
909        let close_case_re = Regex::new(&close_case_pattern).ok()?;
910
911        let cs = case_re.find(content).map(|m| m.start());
912        let ds = default_re.find(content).map(|m| m.start());
913        let ccs = close_case_re.find(content).map(|m| m.start());
914
915        // 找到最近的位置
916        let mut candidates: Vec<(usize, bool)> = Vec::new();
917        if let Some(p) = cs {
918            candidates.push((p, false));
919        }
920        if let Some(p) = ds {
921            candidates.push((p, false));
922        }
923        if let Some(p) = ccs {
924            candidates.push((p, true));
925        }
926
927        candidates.into_iter().min_by_key(|(p, _)| *p)
928    }
929
930    // ========================================================================
931    // {for} — 对齐 PHP `tagFor`
932    // ========================================================================
933
934    /// 渲染 {for}...{/for} 块
935    ///
936    /// PHP `tagFor` 用 `rand()` 生成唯一临时变量名防止嵌套冲突。
937    /// Rust 不需要临时变量(直接求值),但仍保留 `$name` 变量供循环体使用。
938    ///
939    /// 属性说明:
940    /// - `start` — 起始值(默认 0)
941    /// - `end` — 结束值(默认 0)
942    /// - `comparison` — 比较类型(默认 `lt`,即 `<`)
943    /// - `step` — 步长(默认 1)
944    /// - `name` — 循环变量名(默认 `i`)
945    fn render_for(
946        &self,
947        tag_inner: &str,
948        block_content: &str,
949        data: &ViewData,
950    ) -> Result<String, ViewError> {
951        let attrs = parse_attributes(tag_inner);
952
953        let start: f64 = attrs
954            .get("start")
955            .and_then(|v| parse_operand_value(v, data).as_f64())
956            .unwrap_or(0.0);
957        let end: f64 = attrs
958            .get("end")
959            .and_then(|v| parse_operand_value(v, data).as_f64())
960            .unwrap_or(0.0);
961        let step: f64 = attrs
962            .get("step")
963            .and_then(|v| parse_operand_value(v, data).as_f64())
964            .unwrap_or(1.0);
965        let comparison = attrs
966            .get("comparison")
967            .cloned()
968            .unwrap_or_else(|| "lt".to_string());
969        let name = attrs
970            .get("name")
971            .cloned()
972            .unwrap_or_else(|| "i".to_string());
973
974        // 防止死循环:step 必须非零
975        if step == 0.0 {
976            return Err(ViewError::SyntaxError("{for} step 不能为 0".to_string()));
977        }
978
979        let mut result = String::new();
980        let mut current = start;
981
982        loop {
983            // 检查循环条件
984            let should_continue = match comparison.as_str() {
985                "lt" | "<" => current < end,
986                "elt" | "<=" => current <= end,
987                "gt" | ">" => current > end,
988                "egt" | ">=" => current >= end,
989                "eq" | "==" => current == end,
990                "neq" | "!=" => current != end,
991                "heq" | "===" => current == end,
992                "nheq" | "!==" => current != end,
993                _ => current < end, // 默认 lt
994            };
995
996            if !should_continue {
997                break;
998            }
999
1000            // 设置循环变量
1001            let mut child_data = data.clone();
1002            child_data.insert(
1003                name.clone(),
1004                serde_json::Number::from_f64(current)
1005                    .map(Value::Number)
1006                    .unwrap_or(Value::Null),
1007            );
1008
1009            let rendered = (self.render_inner)(block_content, &child_data)?;
1010            result.push_str(&rendered);
1011
1012            current += step;
1013
1014            // 安全限制:防止无限循环
1015            if result.len() > 10_000_000 {
1016                return Err(ViewError::RenderError(
1017                    "{for} 循环输出超过 10MB 限制".to_string(),
1018                ));
1019            }
1020        }
1021
1022        Ok(result)
1023    }
1024}
1025
1026// ============================================================================
1027// switch case 辅助类型
1028// ============================================================================
1029
1030/// switch case 的种类
1031#[derive(Debug, Clone)]
1032enum CaseKind {
1033    /// {case value="1|2|3"} — 值列表
1034    Values(Vec<String>),
1035    /// {default /}
1036    Default,
1037}
1038
1039/// 一个 switch case 分支
1040#[derive(Debug, Clone)]
1041struct SwitchCase {
1042    kind: CaseKind,
1043    content: String,
1044    break_attr: Option<String>,
1045}
1046
1047impl SwitchCase {
1048    /// 是否应该 break(对齐 PHP `tagCase` 的 break 逻辑)
1049    ///
1050    /// PHP 逻辑:
1051    /// ```php
1052    /// $isBreak = isset($tag['break']) ? $tag['break'] : '';
1053    /// if ('' == $isBreak || $isBreak) { break; }
1054    /// ```
1055    /// - 无 break 属性 → break(默认)
1056    /// - break="" → break(空字符串等于 '')
1057    /// - break="false" → break("false" 是 truthy)
1058    /// - break="0" → 不 break("0" 是 falsy)
1059    fn should_break(&self) -> bool {
1060        match &self.break_attr {
1061            None => true, // 默认 break
1062            Some(v) => is_truthy(&Value::String(v.clone())),
1063        }
1064    }
1065}
1066
1067// ============================================================================
1068// 条件求值器 — 对齐 PHP `parseCondition` + `eval`
1069// ============================================================================
1070
1071/// 求值条件表达式
1072///
1073/// PHP 通过 `parseCondition` 替换简写后,用 `eval` 执行。
1074/// Rust 改为递归下降求值器,支持 `||`、`&&`、`!`、比较运算符和括号。
1075pub fn evaluate_condition(condition: &str, data: &ViewData) -> Result<bool, ViewError> {
1076    let condition = replace_condition_shorthands(condition.trim());
1077    evaluate_or(&condition, data)
1078}
1079
1080/// 求值 OR 表达式(`||` 分隔)
1081fn evaluate_or(expr: &str, data: &ViewData) -> Result<bool, ViewError> {
1082    for part in split_top_level(expr, "||") {
1083        if evaluate_and(part.trim(), data)? {
1084            return Ok(true);
1085        }
1086    }
1087    Ok(false)
1088}
1089
1090/// 求值 AND 表达式(`&&` 分隔)
1091fn evaluate_and(expr: &str, data: &ViewData) -> Result<bool, ViewError> {
1092    for part in split_top_level(expr, "&&") {
1093        if !evaluate_not(part.trim(), data)? {
1094            return Ok(false);
1095        }
1096    }
1097    Ok(true)
1098}
1099
1100/// 求值 NOT 表达式(`!` 前缀)
1101fn evaluate_not(expr: &str, data: &ViewData) -> Result<bool, ViewError> {
1102    let expr = expr.trim();
1103    if let Some(rest) = expr.strip_prefix('!') {
1104        return Ok(!evaluate_not(rest.trim(), data)?);
1105    }
1106    evaluate_comparison(expr, data)
1107}
1108
1109/// 求值比较表达式
1110///
1111/// 支持运算符(按优先匹配长度排序):`===`、`!==`、`==`、`!=`、`>=`、`<=`、`>`、`<`
1112fn evaluate_comparison(expr: &str, data: &ViewData) -> Result<bool, ViewError> {
1113    let expr = expr.trim();
1114
1115    // 去掉外层括号
1116    if expr.starts_with('(') && expr.ends_with(')') && is_balanced_parens(&expr[1..expr.len() - 1])
1117    {
1118        return evaluate_or(&expr[1..expr.len() - 1], data);
1119    }
1120
1121    // 查找比较运算符(在顶层,不在括号或引号内)
1122    if let Some((op, pos)) = find_comparison_operator(expr) {
1123        let left = expr[..pos].trim();
1124        let right = expr[pos + op.len()..].trim();
1125        let left_val = parse_operand_value(left, data);
1126        let right_val = parse_operand_value(right, data);
1127        return Ok(compare_values(&left_val, op, &right_val));
1128    }
1129
1130    // 无运算符:检查真值
1131    let val = parse_operand_value(expr, data);
1132    Ok(is_truthy(&val))
1133}
1134
1135/// 在顶层查找比较运算符(跳过括号和引号内的)
1136fn find_comparison_operator(expr: &str) -> Option<(&'static str, usize)> {
1137    let bytes = expr.as_bytes();
1138    let mut depth = 0;
1139    let mut in_single = false;
1140    let mut in_double = false;
1141
1142    let operators: &[&'static str] = &["===", "!==", "==", "!=", ">=", "<=", ">", "<"];
1143
1144    let mut i = 0;
1145    while i < bytes.len() {
1146        match bytes[i] {
1147            b'(' if !in_single && !in_double => {
1148                depth += 1;
1149            }
1150            b')' if !in_single && !in_double => {
1151                depth -= 1;
1152            }
1153            b'\'' if !in_double => {
1154                in_single = !in_single;
1155            }
1156            b'"' if !in_single => {
1157                in_double = !in_double;
1158            }
1159            _ if depth == 0 && !in_single && !in_double => {
1160                let rest = &expr[i..];
1161                for op in operators {
1162                    if rest.starts_with(op) {
1163                        return Some((op, i));
1164                    }
1165                }
1166            }
1167            _ => {}
1168        }
1169        i += 1;
1170    }
1171    None
1172}
1173
1174/// 检查括号是否平衡
1175fn is_balanced_parens(expr: &str) -> bool {
1176    let mut depth = 0;
1177    for c in expr.chars() {
1178        match c {
1179            '(' => depth += 1,
1180            ')' => {
1181                depth -= 1;
1182                if depth < 0 {
1183                    return false;
1184                }
1185            }
1186            _ => {}
1187        }
1188    }
1189    depth == 0
1190}
1191
1192/// 在顶层分隔字符串(跳过括号和引号内的分隔符)
1193fn split_top_level<'a>(expr: &'a str, sep: &str) -> Vec<&'a str> {
1194    let mut parts = Vec::new();
1195    let mut depth = 0;
1196    let mut in_single = false;
1197    let mut in_double = false;
1198    let mut last = 0;
1199
1200    let bytes = expr.as_bytes();
1201    let sep_bytes = sep.as_bytes();
1202    let sep_len = sep_bytes.len();
1203
1204    let mut i = 0;
1205    while i < bytes.len() {
1206        match bytes[i] {
1207            b'(' if !in_single && !in_double => depth += 1,
1208            b')' if !in_single && !in_double => depth -= 1,
1209            b'\'' if !in_double => in_single = !in_single,
1210            b'"' if !in_single => in_double = !in_double,
1211            _ if depth == 0
1212                && !in_single
1213                && !in_double
1214                && i + sep_len <= bytes.len()
1215                && &bytes[i..i + sep_len] == sep_bytes =>
1216            {
1217                parts.push(&expr[last..i]);
1218                last = i + sep_len;
1219                i += sep_len;
1220                continue;
1221            }
1222            _ => {}
1223        }
1224        i += 1;
1225    }
1226    parts.push(&expr[last..]);
1227    parts
1228}
1229
1230// ============================================================================
1231// 值解析与比较
1232// ============================================================================
1233
1234/// 解析操作数为 Value(对齐 PHP `autoBuildVar`)
1235///
1236/// PHP 规则:
1237/// - `:` 开头 → 函数调用(NOTE: 函数调用支持)
1238/// - `$` 开头 → 变量引用
1239/// - 字母/下划线开头 → 自动补 `$` 前缀(视为变量名)
1240/// - 数字 → 数字字面量
1241/// - 引号包裹 → 字符串字面量
1242/// - `true`/`false`/`null` → 对应字面量
1243fn parse_operand_value(expr: &str, data: &ViewData) -> Value {
1244    let expr = expr.trim();
1245    if expr.is_empty() {
1246        return Value::Null;
1247    }
1248
1249    // 变量 ($var 或 $var.attr)
1250    if let Some(rest) = expr.strip_prefix('$') {
1251        return resolve_var_expr(rest, data);
1252    }
1253
1254    // 函数调用 (:func())
1255    if expr.starts_with(':') {
1256        // NOTE: 支持条件中的函数调用
1257        return Value::Null;
1258    }
1259
1260    // 引号字符串
1261    if (expr.starts_with('\'') && expr.ends_with('\'') && expr.len() >= 2)
1262        || (expr.starts_with('"') && expr.ends_with('"') && expr.len() >= 2)
1263    {
1264        return Value::String(expr[1..expr.len() - 1].to_string());
1265    }
1266
1267    // 数字
1268    if let Ok(n) = expr.parse::<i64>() {
1269        return Value::Number(n.into());
1270    }
1271    if let Ok(f) = expr.parse::<f64>() {
1272        if let Some(n) = serde_json::Number::from_f64(f) {
1273            return Value::Number(n);
1274        }
1275    }
1276
1277    // true / false / null
1278    match expr {
1279        "true" => return Value::Bool(true),
1280        "false" => return Value::Bool(false),
1281        "null" => return Value::Null,
1282        _ => {}
1283    }
1284
1285    // autoBuildVar:字母/下划线开头 → 视为变量名
1286    // 注:expr 已在 1245 行确认非空,next() 必返回 Some;
1287    // 使用 match 而非 unwrap() 以保持防御性(避免未来重构破坏不变量时 panic)
1288    match expr.chars().next() {
1289        Some(first) if first.is_alphabetic() || first == '_' => {
1290            return resolve_var_expr(expr, data);
1291        }
1292        _ => {}
1293    }
1294
1295    Value::Null
1296}
1297
1298/// 比较两个 Value(对齐 PHP 比较语义)
1299fn compare_values(left: &Value, op: &str, right: &Value) -> bool {
1300    match op {
1301        "==" => loose_equal(left, right),
1302        "!=" => !loose_equal(left, right),
1303        "===" => strict_equal(left, right),
1304        "!==" => !strict_equal(left, right),
1305        ">" => value_to_f64(left) > value_to_f64(right),
1306        "<" => value_to_f64(left) < value_to_f64(right),
1307        ">=" => value_to_f64(left) >= value_to_f64(right),
1308        "<=" => value_to_f64(left) <= value_to_f64(right),
1309        _ => false,
1310    }
1311}
1312
1313/// PHP 松散相等(`==`)
1314///
1315/// - 数字与数字:数值比较
1316/// - 字符串与字符串:字符串比较
1317/// - 数字与字符串:尝试将字符串转数字比较,否则按字符串比较
1318/// - 其他:先转 bool 再比较
1319fn loose_equal(left: &Value, right: &Value) -> bool {
1320    match (left, right) {
1321        (Value::Null, Value::Null) => true,
1322        (Value::Bool(a), Value::Bool(b)) => a == b,
1323        (Value::Number(a), Value::Number(b)) => a == b,
1324        (Value::String(a), Value::String(b)) => a == b,
1325        (Value::Number(a), Value::String(b)) => {
1326            if let Ok(n) = b.parse::<f64>() {
1327                a.as_f64() == Some(n)
1328            } else {
1329                a.to_string() == *b
1330            }
1331        }
1332        (Value::String(a), Value::Number(b)) => {
1333            if let Ok(n) = a.parse::<f64>() {
1334                Some(n) == b.as_f64()
1335            } else {
1336                *a == b.to_string()
1337            }
1338        }
1339        (Value::Bool(a), Value::Number(b)) => *a == (b.as_f64().unwrap_or(0.0) != 0.0),
1340        (Value::Number(a), Value::Bool(b)) => (a.as_f64().unwrap_or(0.0) != 0.0) == *b,
1341        (Value::Bool(a), Value::String(b)) => *a == is_truthy(&Value::String(b.clone())),
1342        (Value::String(a), Value::Bool(b)) => is_truthy(&Value::String(a.clone())) == *b,
1343        _ => left == right,
1344    }
1345}
1346
1347/// PHP 严格相等(`===`)— 类型和值都必须相同
1348fn strict_equal(left: &Value, right: &Value) -> bool {
1349    left == right
1350}
1351
1352/// Value 转 f64(对齐 PHP 数值转换)
1353fn value_to_f64(value: &Value) -> f64 {
1354    match value {
1355        Value::Number(n) => n.as_f64().unwrap_or(0.0),
1356        Value::String(s) => s.parse::<f64>().unwrap_or(0.0),
1357        Value::Bool(b) => {
1358            if *b {
1359                1.0
1360            } else {
1361                0.0
1362            }
1363        }
1364        Value::Null => 0.0,
1365        Value::Array(a) => a.len() as f64,
1366        Value::Object(o) => o.len() as f64,
1367    }
1368}
1369
1370// ============================================================================
1371// 辅助函数
1372// ============================================================================
1373
1374/// 解析标签属性(对齐 PHP `TagLib::parseXmlTag` 属性解析)
1375///
1376/// 支持双引号和单引号:`key="value"` 或 `key='value'`
1377fn parse_attributes(tag: &str) -> HashMap<String, String> {
1378    let mut attrs = HashMap::new();
1379    let re = Regex::new(r#"(\w+)=["']([^"']*)["']"#).unwrap_or_else(|e| panic!("attr regex: {e}"));
1380    for cap in re.captures_iter(tag) {
1381        attrs.insert(cap[1].to_string(), cap[2].to_string());
1382    }
1383    attrs
1384}
1385
1386/// 替换条件简写(对齐 PHP `TagLib::parseCondition` 的 `str_ireplace`)
1387///
1388/// PHP `$comparison` 映射(大小写不敏感):
1389/// - ` eq ` → ` == `
1390/// - ` neq ` → ` != `
1391/// - ` gt ` → ` > `
1392/// - ` lt ` → ` < `
1393/// - ` egt ` → ` >= `
1394/// - ` elt ` → ` <= `
1395/// - ` heq ` → ` === `
1396/// - ` nheq ` → ` !== `
1397fn replace_condition_shorthands(condition: &str) -> String {
1398    // PHP 使用 str_ireplace(大小写不敏感),Rust 用 to_lowercase 模拟
1399    let mut result = condition.to_string();
1400
1401    // 按长度降序替换,避免短简写覆盖长简写(如 "nheq" 被 "heq" 部分匹配)
1402    let replacements: &[(&str, &str)] = &[
1403        (" nheq ", " !== "),
1404        (" heq ", " === "),
1405        (" neq ", " != "),
1406        (" egt ", " >= "),
1407        (" elt ", " <= "),
1408        (" eq ", " == "),
1409        (" gt ", " > "),
1410        (" lt ", " < "),
1411    ];
1412
1413    for (from, to) in replacements {
1414        let mut search_pos = 0;
1415        loop {
1416            // 每次重新生成 lower,确保位置与 result 同步
1417            let lower = result.to_lowercase();
1418            match lower[search_pos..].find(from) {
1419                Some(pos) => {
1420                    let abs_pos = search_pos + pos;
1421                    result.replace_range(abs_pos..abs_pos + from.len(), to);
1422                    search_pos = abs_pos + to.len();
1423                }
1424                None => break,
1425            }
1426        }
1427    }
1428
1429    result
1430}
1431
1432/// 解析变量名(对齐 PHP `autoBuildVar`)
1433///
1434/// - `:` 开头 → 函数调用(NOTE: 暂不支持)
1435/// - `$` 开头 → 去掉 `$` 后解析变量
1436/// - 其他 → 直接作为变量名解析
1437fn resolve_name(name: &str, data: &ViewData) -> Value {
1438    let name = name.trim();
1439    if name.is_empty() {
1440        return Value::Null;
1441    }
1442
1443    // 函数调用
1444    if name.starts_with(':') {
1445        // NOTE: 支持函数调用
1446        return Value::Null;
1447    }
1448
1449    // 去掉 $ 前缀
1450    let var_name = name.strip_prefix('$').unwrap_or(name);
1451    resolve_var_expr(var_name, data)
1452}
1453
1454/// 解析 case value(对齐 PHP `tagCase` 的 value 解析)
1455///
1456/// - `$` 开头 → 变量引用
1457/// - `:` 开头 → 函数调用(NOTE)
1458/// - 其他 → 字面量
1459fn resolve_case_value(value: &str, data: &ViewData) -> Value {
1460    let value = value.trim();
1461    if value.is_empty() {
1462        return Value::Null;
1463    }
1464
1465    if let Some(rest) = value.strip_prefix('$') {
1466        return resolve_var_expr(rest, data);
1467    }
1468
1469    if value.starts_with(':') {
1470        return Value::Null;
1471    }
1472
1473    parse_literal(value)
1474}
1475
1476// ============================================================================
1477// 测试
1478// ============================================================================
1479
1480#[cfg(test)]
1481mod tests {
1482    use super::*;
1483    use serde_json::json;
1484    use std::collections::HashMap;
1485
1486    /// 创建默认配置
1487    fn make_config() -> ViewConfig {
1488        ViewConfig::default()
1489    }
1490
1491    /// 创建测试数据
1492    fn make_data() -> ViewData {
1493        let mut data = HashMap::new();
1494        data.insert("a".to_string(), json!(1));
1495        data.insert("b".to_string(), json!(2));
1496        data.insert("name".to_string(), json!("Alice"));
1497        data.insert("list".to_string(), json!([1, 2, 3]));
1498        data.insert("empty_list".to_string(), json!([]));
1499        data.insert("obj".to_string(), json!({"x": 10, "y": 20}));
1500        data.insert("var".to_string(), json!(2));
1501        data.insert("flag".to_string(), json!(true));
1502        data
1503    }
1504
1505    /// 渲染辅助函数:使用默认配置和递归内部渲染器
1506    ///
1507    /// render_recursive 先处理控制流标签(递归),再处理 {$var} 标签
1508    fn render(template: &str, data: &ViewData) -> String {
1509        let config = make_config();
1510
1511        fn render_recursive(
1512            content: &str,
1513            d: &ViewData,
1514            config: &ViewConfig,
1515        ) -> Result<String, ViewError> {
1516            // 先处理控制流标签(递归)
1517            let content =
1518                render_control_flow(content, d, config, |c, d2| render_recursive(c, d2, config))?;
1519            // 再处理 {$var} 标签
1520            let begin = &config.tpl_begin;
1521            let end = &config.tpl_end;
1522            let pattern = format!("{}(.*?){}", regex::escape(begin), regex::escape(end));
1523            let re = Regex::new(&pattern).unwrap();
1524            let mut result = String::new();
1525            let mut last = 0;
1526            for caps in re.captures_iter(&content) {
1527                let m = caps.get(0).unwrap();
1528                let tag = caps.get(1).unwrap().as_str().trim();
1529                result.push_str(&content[last..m.start()]);
1530                if let Some(rest) = tag.strip_prefix('$') {
1531                    let val = resolve_var_expr(rest, d);
1532                    result.push_str(&value_to_string(&val));
1533                } else {
1534                    result.push_str(&content[m.start()..m.end()]);
1535                }
1536                last = m.end();
1537            }
1538            result.push_str(&content[last..]);
1539            Ok(result)
1540        }
1541
1542        render_recursive(template, data, &config).unwrap()
1543    }
1544
1545    // =========================================================================
1546    // 组 1:{if} 条件判断
1547    // =========================================================================
1548
1549    #[test]
1550    fn test_if_true() {
1551        let data = make_data();
1552        let result = render("{if condition=\"$a eq 1\"}yes{/if}", &data);
1553        assert_eq!(result, "yes");
1554    }
1555
1556    #[test]
1557    fn test_if_false() {
1558        let data = make_data();
1559        let result = render("{if condition=\"$a eq 2\"}yes{/if}", &data);
1560        assert_eq!(result, "");
1561    }
1562
1563    #[test]
1564    fn test_if_else_true() {
1565        let data = make_data();
1566        let result = render("{if condition=\"$a eq 1\"}yes{else /}no{/if}", &data);
1567        assert_eq!(result, "yes");
1568    }
1569
1570    #[test]
1571    fn test_if_else_false() {
1572        let data = make_data();
1573        let result = render("{if condition=\"$a eq 2\"}yes{else /}no{/if}", &data);
1574        assert_eq!(result, "no");
1575    }
1576
1577    #[test]
1578    fn test_if_elseif_else() {
1579        let data = make_data();
1580        let result = render(
1581            "{if condition=\"$a eq 2\"}two{elseif condition=\"$a eq 1\" /}one{else /}other{/if}",
1582            &data,
1583        );
1584        assert_eq!(result, "one");
1585    }
1586
1587    #[test]
1588    fn test_if_elseif_else_default() {
1589        let data = make_data();
1590        let result = render(
1591            "{if condition=\"$a eq 5\"}five{elseif condition=\"$a eq 6\" /}six{else /}other{/if}",
1592            &data,
1593        );
1594        assert_eq!(result, "other");
1595    }
1596
1597    #[test]
1598    fn test_if_with_neq() {
1599        let data = make_data();
1600        let result = render("{if condition=\"$a neq 2\"}not_two{/if}", &data);
1601        assert_eq!(result, "not_two");
1602    }
1603
1604    #[test]
1605    fn test_if_with_gt_lt() {
1606        let data = make_data();
1607        assert_eq!(render("{if condition=\"$b gt 1\"}big{/if}", &data), "big");
1608        assert_eq!(
1609            render("{if condition=\"$a lt 2\"}small{/if}", &data),
1610            "small"
1611        );
1612    }
1613
1614    #[test]
1615    fn test_if_with_egt_elt() {
1616        let data = make_data();
1617        assert_eq!(render("{if condition=\"$a egt 1\"}ge1{/if}", &data), "ge1");
1618        assert_eq!(render("{if condition=\"$b elt 2\"}le2{/if}", &data), "le2");
1619    }
1620
1621    #[test]
1622    fn test_if_with_heq_nheq() {
1623        let data = make_data();
1624        assert_eq!(
1625            render("{if condition=\"$a heq 1\"}identical{/if}", &data),
1626            "identical"
1627        );
1628        assert_eq!(
1629            render("{if condition=\"$a nheq 2\"}not_identical{/if}", &data),
1630            "not_identical"
1631        );
1632    }
1633
1634    #[test]
1635    fn test_if_expression_form() {
1636        let data = make_data();
1637        let result = render("{if ($a == 1)}match{/if}", &data);
1638        assert_eq!(result, "match");
1639    }
1640
1641    #[test]
1642    fn test_if_with_and() {
1643        let data = make_data();
1644        let result = render("{if condition=\"$a gt 0 && $b lt 5\"}in_range{/if}", &data);
1645        assert_eq!(result, "in_range");
1646    }
1647
1648    #[test]
1649    fn test_if_with_or() {
1650        let data = make_data();
1651        let result = render("{if condition=\"$a eq 5 || $b eq 2\"}match{/if}", &data);
1652        assert_eq!(result, "match");
1653    }
1654
1655    #[test]
1656    fn test_if_with_not() {
1657        let data = make_data();
1658        let result = render("{if condition=\"!$flag\"}not_flag{/if}", &data);
1659        assert_eq!(result, "");
1660    }
1661
1662    #[test]
1663    fn test_if_with_parens() {
1664        let data = make_data();
1665        let result = render("{if condition=\"($a eq 1) && ($b eq 2)\"}both{/if}", &data);
1666        assert_eq!(result, "both");
1667    }
1668
1669    #[test]
1670    fn test_if_case_insensitive_shorthand() {
1671        let data = make_data();
1672        // PHP str_ireplace 是大小写不敏感的
1673        let result = render("{if condition=\"$a EQ 1\"}yes{/if}", &data);
1674        assert_eq!(result, "yes");
1675    }
1676
1677    // =========================================================================
1678    // 组 2:{foreach} 循环
1679    // =========================================================================
1680
1681    #[test]
1682    fn test_foreach_basic() {
1683        let data = make_data();
1684        let result = render(
1685            "{foreach name=\"list\" id=\"item\"}{$item},{/foreach}",
1686            &data,
1687        );
1688        assert_eq!(result, "1,2,3,");
1689    }
1690
1691    #[test]
1692    fn test_foreach_with_key() {
1693        let data = make_data();
1694        let result = render(
1695            "{foreach name=\"list\" id=\"item\" key=\"k\"}{$k}={$item},{/foreach}",
1696            &data,
1697        );
1698        assert_eq!(result, "0=1,1=2,2=3,");
1699    }
1700
1701    #[test]
1702    fn test_foreach_with_index() {
1703        let data = make_data();
1704        let result = render(
1705            "{foreach name=\"list\" id=\"item\" index=\"idx\"}{$idx}:{$item},{/foreach}",
1706            &data,
1707        );
1708        assert_eq!(result, "0:1,1:2,2:3,");
1709    }
1710
1711    #[test]
1712    fn test_foreach_object() {
1713        let data = make_data();
1714        let result = render(
1715            "{foreach name=\"obj\" id=\"v\" key=\"k\"}{$k}={$v},{/foreach}",
1716            &data,
1717        );
1718        assert_eq!(result, "x=10,y=20,");
1719    }
1720
1721    #[test]
1722    fn test_foreach_empty() {
1723        let data = make_data();
1724        let result = render(
1725            "{foreach name=\"empty_list\" id=\"item\" empty=\"no data\"}{$item}{/foreach}",
1726            &data,
1727        );
1728        assert_eq!(result, "no data");
1729    }
1730
1731    #[test]
1732    fn test_foreach_expression_form() {
1733        let data = make_data();
1734        let result = render("{foreach ($list as $k=>$v)}{$k}={$v},{/foreach}", &data);
1735        assert_eq!(result, "0=1,1=2,2=3,");
1736    }
1737
1738    #[test]
1739    fn test_foreach_expression_form_no_key() {
1740        let data = make_data();
1741        let result = render("{foreach ($list as $v)}{$v},{/foreach}", &data);
1742        assert_eq!(result, "1,2,3,");
1743    }
1744
1745    #[test]
1746    fn test_foreach_with_offset() {
1747        let data = make_data();
1748        let result = render(
1749            "{foreach name=\"list\" id=\"item\" offset=\"1\"}{$item},{/foreach}",
1750            &data,
1751        );
1752        assert_eq!(result, "2,3,");
1753    }
1754
1755    #[test]
1756    fn test_foreach_with_length() {
1757        let data = make_data();
1758        let result = render(
1759            "{foreach name=\"list\" id=\"item\" length=\"2\"}{$item},{/foreach}",
1760            &data,
1761        );
1762        assert_eq!(result, "1,2,");
1763    }
1764
1765    // =========================================================================
1766    // 组 3:{volist} 循环
1767    // =========================================================================
1768
1769    #[test]
1770    fn test_volist_basic() {
1771        let data = make_data();
1772        let result = render("{volist name=\"list\" id=\"item\"}{$item},{/volist}", &data);
1773        assert_eq!(result, "1,2,3,");
1774    }
1775
1776    #[test]
1777    fn test_volist_key_is_counter() {
1778        // PHP volist: key 属性是计数器变量名(默认 i,1-based)
1779        let data = make_data();
1780        let result = render(
1781            "{volist name=\"list\" id=\"item\" key=\"i\"}{$i}:{$item},{/volist}",
1782            &data,
1783        );
1784        assert_eq!(result, "1:1,2:2,3:3,");
1785    }
1786
1787    #[test]
1788    fn test_volist_key_default_is_i() {
1789        let data = make_data();
1790        let result = render(
1791            "{volist name=\"list\" id=\"item\" key=\"counter\"}{$counter}:{$item},{/volist}",
1792            &data,
1793        );
1794        assert_eq!(result, "1:1,2:2,3:3,");
1795    }
1796
1797    #[test]
1798    fn test_volist_array_key_variable() {
1799        // PHP volist: 数组键总是赋给字面变量 $key
1800        let data = make_data();
1801        let result = render(
1802            "{volist name=\"list\" id=\"item\"}key={$key},item={$item};{/volist}",
1803            &data,
1804        );
1805        assert_eq!(result, "key=0,item=1;key=1,item=2;key=2,item=3;");
1806    }
1807
1808    #[test]
1809    fn test_volist_empty() {
1810        let data = make_data();
1811        let result = render(
1812            "{volist name=\"empty_list\" id=\"item\" empty=\"no data\"}{$item}{/volist}",
1813            &data,
1814        );
1815        assert_eq!(result, "no data");
1816    }
1817
1818    #[test]
1819    fn test_volist_with_offset() {
1820        let data = make_data();
1821        let result = render(
1822            "{volist name=\"list\" id=\"item\" offset=\"1\"}{$item},{/volist}",
1823            &data,
1824        );
1825        assert_eq!(result, "2,3,");
1826    }
1827
1828    #[test]
1829    fn test_volist_with_length() {
1830        let data = make_data();
1831        let result = render(
1832            "{volist name=\"list\" id=\"item\" length=\"2\"}{$item},{/volist}",
1833            &data,
1834        );
1835        assert_eq!(result, "1,2,");
1836    }
1837
1838    // =========================================================================
1839    // 组 4:{switch}/{case}/{default}
1840    // =========================================================================
1841
1842    #[test]
1843    fn test_switch_case_match() {
1844        let data = make_data();
1845        let result = render(
1846            "{switch name=\"var\"}{case value=\"1\"}one{/case}{case value=\"2\"}two{/case}{default /}other{/switch}",
1847            &data,
1848        );
1849        assert_eq!(result, "two");
1850    }
1851
1852    #[test]
1853    fn test_switch_default() {
1854        let data = make_data();
1855        let result = render(
1856            "{switch name=\"var\"}{case value=\"5\"}five{/case}{default /}other{/switch}",
1857            &data,
1858        );
1859        assert_eq!(result, "other");
1860    }
1861
1862    #[test]
1863    fn test_switch_case_multi_value() {
1864        let data = make_data();
1865        let result = render(
1866            "{switch name=\"var\"}{case value=\"1|2|3\"}low{/case}{default /}high{/switch}",
1867            &data,
1868        );
1869        assert_eq!(result, "low");
1870    }
1871
1872    #[test]
1873    fn test_switch_case_no_close_tag() {
1874        // 不使用 {/case} 闭合
1875        let data = make_data();
1876        let result = render(
1877            "{switch name=\"var\"}{case value=\"1\"}one{case value=\"2\"}two{default /}other{/switch}",
1878            &data,
1879        );
1880        assert_eq!(result, "two");
1881    }
1882
1883    #[test]
1884    fn test_switch_case_break_false_still_breaks() {
1885        // PHP 陷阱:break="false" 中 "false" 是 truthy,实际会 break
1886        let data = make_data();
1887        let result = render(
1888            "{switch name=\"var\"}{case value=\"2\" break=\"false\"}two{/case}{case value=\"2\"}also_two{/case}{default /}other{/switch}",
1889            &data,
1890        );
1891        assert_eq!(result, "two");
1892    }
1893
1894    #[test]
1895    fn test_switch_case_break_zero_fall_through() {
1896        // break="0" 中 "0" 是 falsy,实现贯穿
1897        let data = make_data();
1898        let result = render(
1899            "{switch name=\"var\"}{case value=\"2\" break=\"0\"}two_{/case}{case value=\"2\"}also_two{/case}{default /}other{/switch}",
1900            &data,
1901        );
1902        assert_eq!(result, "two_also_two");
1903    }
1904
1905    // =========================================================================
1906    // 组 5:{for} 循环
1907    // =========================================================================
1908
1909    #[test]
1910    fn test_for_basic() {
1911        let data = make_data();
1912        let result = render("{for start=\"0\" end=\"3\" name=\"i\"}{$i},{/for}", &data);
1913        assert_eq!(result, "0,1,2,");
1914    }
1915
1916    #[test]
1917    fn test_for_with_step() {
1918        let data = make_data();
1919        let result = render(
1920            "{for start=\"0\" end=\"10\" step=\"3\" name=\"i\"}{$i},{/for}",
1921            &data,
1922        );
1923        assert_eq!(result, "0,3,6,9,");
1924    }
1925
1926    #[test]
1927    fn test_for_default_name() {
1928        let data = make_data();
1929        let result = render("{for start=\"1\" end=\"3\"}{$i},{/for}", &data);
1930        assert_eq!(result, "1,2,");
1931    }
1932
1933    #[test]
1934    fn test_for_comparison_elt() {
1935        let data = make_data();
1936        let result = render(
1937            "{for start=\"0\" end=\"3\" comparison=\"elt\" name=\"i\"}{$i},{/for}",
1938            &data,
1939        );
1940        assert_eq!(result, "0,1,2,3,");
1941    }
1942
1943    #[test]
1944    fn test_for_comparison_gt() {
1945        let data = make_data();
1946        let result = render(
1947            "{for start=\"5\" end=\"0\" step=\"-1\" comparison=\"gt\" name=\"i\"}{$i},{/for}",
1948            &data,
1949        );
1950        assert_eq!(result, "5,4,3,2,1,");
1951    }
1952
1953    // =========================================================================
1954    // 组 6:嵌套控制流
1955    // =========================================================================
1956
1957    #[test]
1958    fn test_nested_if_in_foreach() {
1959        let data = make_data();
1960        let result = render(
1961            "{foreach name=\"list\" id=\"item\"}{if condition=\"$item gt 1\"}big{else /}small{/if},{/foreach}",
1962            &data,
1963        );
1964        assert_eq!(result, "small,big,big,");
1965    }
1966
1967    #[test]
1968    fn test_nested_foreach() {
1969        let mut data = make_data();
1970        data.insert("matrix".to_string(), json!([[1, 2], [3, 4]]));
1971        let result = render(
1972            "{foreach name=\"matrix\" id=\"row\"}{foreach name=\"row\" id=\"col\"}{$col} {/foreach}| {/foreach}",
1973            &data,
1974        );
1975        assert_eq!(result, "1 2 | 3 4 | ");
1976    }
1977
1978    #[test]
1979    fn test_nested_if() {
1980        let data = make_data();
1981        let result = render(
1982            "{if condition=\"$a eq 1\"}{if condition=\"$b eq 2\"}both{/if}{/if}",
1983            &data,
1984        );
1985        assert_eq!(result, "both");
1986    }
1987
1988    #[test]
1989    fn test_switch_in_foreach() {
1990        let data = make_data();
1991        let result = render(
1992            "{foreach name=\"list\" id=\"item\"}{switch name=\"item\"}{case value=\"1\"}one{/case}{case value=\"2\"}two{/case}{default /}other{/switch},{/foreach}",
1993            &data,
1994        );
1995        assert_eq!(result, "one,two,other,");
1996    }
1997
1998    // =========================================================================
1999    // 组 7:条件求值器单元测试
2000    // =========================================================================
2001
2002    #[test]
2003    fn test_evaluate_condition_eq() {
2004        let data = make_data();
2005        assert!(evaluate_condition("$a eq 1", &data).unwrap());
2006        assert!(!evaluate_condition("$a eq 2", &data).unwrap());
2007    }
2008
2009    #[test]
2010    fn test_evaluate_condition_neq() {
2011        let data = make_data();
2012        assert!(evaluate_condition("$a neq 2", &data).unwrap());
2013    }
2014
2015    #[test]
2016    fn test_evaluate_condition_and() {
2017        let data = make_data();
2018        assert!(evaluate_condition("$a eq 1 && $b eq 2", &data).unwrap());
2019        assert!(!evaluate_condition("$a eq 1 && $b eq 3", &data).unwrap());
2020    }
2021
2022    #[test]
2023    fn test_evaluate_condition_or() {
2024        let data = make_data();
2025        assert!(evaluate_condition("$a eq 5 || $b eq 2", &data).unwrap());
2026        assert!(!evaluate_condition("$a eq 5 || $b eq 5", &data).unwrap());
2027    }
2028
2029    #[test]
2030    fn test_evaluate_condition_not() {
2031        let data = make_data();
2032        assert!(!evaluate_condition("!$flag", &data).unwrap());
2033        assert!(!evaluate_condition("!$a", &data).unwrap()); // $a=1 is truthy
2034    }
2035
2036    #[test]
2037    fn test_evaluate_condition_parens() {
2038        let data = make_data();
2039        assert!(evaluate_condition("($a eq 1) && ($b eq 2)", &data).unwrap());
2040    }
2041
2042    #[test]
2043    fn test_evaluate_condition_truthy_var() {
2044        let data = make_data();
2045        assert!(evaluate_condition("$flag", &data).unwrap());
2046        assert!(evaluate_condition("$name", &data).unwrap());
2047    }
2048
2049    #[test]
2050    fn test_evaluate_condition_strict_equal() {
2051        let data = make_data();
2052        assert!(evaluate_condition("$a heq 1", &data).unwrap());
2053        assert!(!evaluate_condition("$a heq '1'", &data).unwrap());
2054    }
2055
2056    // =========================================================================
2057    // 组 8:辅助函数单元测试
2058    // =========================================================================
2059
2060    #[test]
2061    fn test_replace_shorthands_basic() {
2062        assert_eq!(replace_condition_shorthands("$a eq 1"), "$a == 1");
2063        assert_eq!(replace_condition_shorthands("$a neq 1"), "$a != 1");
2064        assert_eq!(replace_condition_shorthands("$a gt 1"), "$a > 1");
2065        assert_eq!(replace_condition_shorthands("$a lt 1"), "$a < 1");
2066        assert_eq!(replace_condition_shorthands("$a egt 1"), "$a >= 1");
2067        assert_eq!(replace_condition_shorthands("$a elt 1"), "$a <= 1");
2068        assert_eq!(replace_condition_shorthands("$a heq 1"), "$a === 1");
2069        assert_eq!(replace_condition_shorthands("$a nheq 1"), "$a !== 1");
2070    }
2071
2072    #[test]
2073    fn test_replace_shorthands_case_insensitive() {
2074        assert_eq!(replace_condition_shorthands("$a EQ 1"), "$a == 1");
2075        assert_eq!(replace_condition_shorthands("$a NEQ 1"), "$a != 1");
2076    }
2077
2078    #[test]
2079    fn test_replace_shorthands_multiple() {
2080        assert_eq!(
2081            replace_condition_shorthands("$a eq 1 && $b neq 2"),
2082            "$a == 1 && $b != 2"
2083        );
2084    }
2085
2086    #[test]
2087    fn test_parse_attributes() {
2088        let attrs = parse_attributes("name=\"list\" id=\"item\" key=\"k\"");
2089        assert_eq!(attrs.get("name"), Some(&"list".to_string()));
2090        assert_eq!(attrs.get("id"), Some(&"item".to_string()));
2091        assert_eq!(attrs.get("key"), Some(&"k".to_string()));
2092    }
2093
2094    #[test]
2095    fn test_parse_attributes_single_quote() {
2096        let attrs = parse_attributes("name='list' id='item'");
2097        assert_eq!(attrs.get("name"), Some(&"list".to_string()));
2098        assert_eq!(attrs.get("id"), Some(&"item".to_string()));
2099    }
2100
2101    #[test]
2102    fn test_parse_attributes_empty() {
2103        let attrs = parse_attributes("");
2104        assert!(attrs.is_empty());
2105    }
2106
2107    #[test]
2108    fn test_loose_equal_numbers() {
2109        assert!(loose_equal(&json!(1), &json!(1)));
2110        assert!(!loose_equal(&json!(1), &json!(2)));
2111    }
2112
2113    #[test]
2114    fn test_loose_equal_number_string() {
2115        assert!(loose_equal(&json!(1), &json!("1")));
2116        assert!(loose_equal(&json!("1"), &json!(1)));
2117    }
2118
2119    #[test]
2120    fn test_loose_equal_strings() {
2121        assert!(loose_equal(&json!("hello"), &json!("hello")));
2122        assert!(!loose_equal(&json!("hello"), &json!("world")));
2123    }
2124
2125    #[test]
2126    fn test_strict_equal() {
2127        assert!(strict_equal(&json!(1), &json!(1)));
2128        assert!(!strict_equal(&json!(1), &json!("1")));
2129        assert!(!strict_equal(&json!(1), &json!(true)));
2130    }
2131
2132    #[test]
2133    fn test_split_top_level_and() {
2134        let parts = split_top_level("$a eq 1 && $b eq 2", "&&");
2135        assert_eq!(parts.len(), 2);
2136        assert_eq!(parts[0].trim(), "$a eq 1");
2137        assert_eq!(parts[1].trim(), "$b eq 2");
2138    }
2139
2140    #[test]
2141    fn test_split_top_level_or() {
2142        let parts = split_top_level("$a eq 1 || $b eq 2", "||");
2143        assert_eq!(parts.len(), 2);
2144    }
2145
2146    #[test]
2147    fn test_split_top_level_with_parens() {
2148        let parts = split_top_level("($a eq 1 || $b eq 2) && $c eq 3", "&&");
2149        assert_eq!(parts.len(), 2);
2150        assert_eq!(parts[0].trim(), "($a eq 1 || $b eq 2)");
2151        assert_eq!(parts[1].trim(), "$c eq 3");
2152    }
2153
2154    #[test]
2155    fn test_find_comparison_operator() {
2156        assert_eq!(find_comparison_operator("$a == 1"), Some(("==", 3)));
2157        assert_eq!(find_comparison_operator("$a === 1"), Some(("===", 3)));
2158        assert_eq!(find_comparison_operator("$a >= 1"), Some((">=", 3)));
2159    }
2160
2161    #[test]
2162    fn test_find_comparison_operator_in_parens() {
2163        // 运算符在括号内不应被找到
2164        assert!(find_comparison_operator("($a == 1)").is_none());
2165    }
2166
2167    // =========================================================================
2168    // 组 9:SwitchCase should_break 逻辑
2169    // =========================================================================
2170
2171    #[test]
2172    fn test_should_break_default() {
2173        let case = SwitchCase {
2174            kind: CaseKind::Default,
2175            content: String::new(),
2176            break_attr: None,
2177        };
2178        assert!(case.should_break()); // 默认 break
2179    }
2180
2181    #[test]
2182    fn test_should_break_false_string() {
2183        // PHP 陷阱:break="false" 中 "false" 是 truthy
2184        let case = SwitchCase {
2185            kind: CaseKind::Values(vec!["1".to_string()]),
2186            content: String::new(),
2187            break_attr: Some("false".to_string()),
2188        };
2189        assert!(case.should_break()); // "false" 是 truthy,仍 break
2190    }
2191
2192    #[test]
2193    fn test_should_break_zero_string() {
2194        // break="0" 中 "0" 是 falsy
2195        let case = SwitchCase {
2196            kind: CaseKind::Values(vec!["1".to_string()]),
2197            content: String::new(),
2198            break_attr: Some("0".to_string()),
2199        };
2200        assert!(!case.should_break()); // "0" 是 falsy,不 break(贯穿)
2201    }
2202
2203    #[test]
2204    fn test_should_break_empty_string() {
2205        // break="" 等于 PHP '',是 falsy
2206        let case = SwitchCase {
2207            kind: CaseKind::Values(vec!["1".to_string()]),
2208            content: String::new(),
2209            break_attr: Some("".to_string()),
2210        };
2211        // PHP: '' == '' → true,但 PHP 真值中空字符串是 false
2212        // PHP 代码: if ('' == $isBreak || $isBreak) → '' == '' 为 true → break
2213        assert!(!case.should_break()); // is_truthy("") = false
2214    }
2215
2216    // =========================================================================
2217    // 组 10:边界情况
2218    // =========================================================================
2219
2220    #[test]
2221    fn test_no_control_flow_tags() {
2222        let data = make_data();
2223        let result = render("hello world", &data);
2224        assert_eq!(result, "hello world");
2225    }
2226
2227    #[test]
2228    fn test_control_flow_with_static_text() {
2229        let data = make_data();
2230        let result = render("before {if condition=\"$a eq 1\"}mid{/if} after", &data);
2231        assert_eq!(result, "before mid after");
2232    }
2233
2234    #[test]
2235    fn test_multiple_control_flow_blocks() {
2236        let data = make_data();
2237        let result = render(
2238            "{if condition=\"$a eq 1\"}A{/if}{if condition=\"$b eq 2\"}B{/if}",
2239            &data,
2240        );
2241        assert_eq!(result, "AB");
2242    }
2243
2244    #[test]
2245    fn test_for_zero_iterations() {
2246        let data = make_data();
2247        let result = render("{for start=\"5\" end=\"3\" name=\"i\"}{$i}{/for}", &data);
2248        assert_eq!(result, "");
2249    }
2250
2251    #[test]
2252    fn test_foreach_empty_no_empty_attr() {
2253        let data = make_data();
2254        let result = render(
2255            "{foreach name=\"empty_list\" id=\"item\"}{$item}{/foreach}",
2256            &data,
2257        );
2258        assert_eq!(result, "");
2259    }
2260}