# EasyTL **Repository Path**: dromara/easy-tl ## Basic Information - **Project Name**: EasyTL - **Description**: 一个功能丰富的轻量级字符串模板引擎,支持类 JavaScript 的表达式语法、控制流语句(if/for/switch)、高级特性(空安全操作符、Elvis 表达式、正则匹配、区间字面量等) - **Primary Language**: Java - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 2 - **Forks**: 2 - **Created**: 2025-11-09 - **Last Updated**: 2025-12-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # EasyTL - 轻量级字符串模板引擎 ## 项目简介 EasyTL 是一个轻量级的字符串模板引擎,基于 Java 8 开发,无第三方依赖,提供类似 JavaScript 的表达式语法支持。 ## 技术栈 - **Java**: JDK 8+ - **构建工具**: Maven - **依赖**: 无第三方依赖 ## 功能特性 ### 1. 纯文本支持 支持直接编写普通字符串文本内容,原样输出。 **示例:** ``` Hello World! 这是一段普通文本。 ``` **输出:** ``` Hello World! 这是一段普通文本。 ``` ### 2. 表达式嵌入 支持在文本中嵌入表达式,支持三种语法格式: #### 2.1 单花括号语法 `{表达式}` ``` Hello {user.name}! ``` #### 2.2 双花括号语法 `{{表达式}}` ``` Hello {{user.name}}! ``` #### 2.3 美元符号语法 `${表达式}` ``` Hello ${user.name}! ``` #### 2.4 语法等价性 以上三种语法格式在功能上完全等价,都可以用于嵌入表达式: ``` // 以下三种写法效果相同 Hello {user.name}! Hello {{user.name}}! Hello ${user.name}! ``` #### 2.5 Token类型和ASTNode类型差异 虽然三种语法格式功能等价,但它们在解析过程中会产生不同的Token类型和ASTNode类型: - `{表达式}` → 生成 `SINGLE_BRACE_TOKEN` 和 `SingleBraceExpressionNode` - `{{表达式}}` → 生成 `DOUBLE_BRACE_TOKEN` 和 `DoubleBraceExpressionNode` - `${表达式}` → 生成 `DOLLAR_BRACE_TOKEN` 和 `DollarBraceExpressionNode` 这种设计允许在后续处理中区分不同的语法来源,便于调试、分析和特殊处理。 #### 2.6 格式兼容性 字符串模板中的纯文本部分支持 JSON、XML 等各种格式,JSON 字符串中的花括号 `{}` 会被视为纯文本,不会与 `{表达式}` 语法产生冲突 **格式兼容示例:** ``` // JSON 格式支持 {"name": "{user.name}", "age": {user.age}, "city": "北京"} // 输出示例:{"name": "张三", "age": 25, "city": "北京"} // XML 格式支持 {user.name}{user.age} // 输出示例:张三25 // 复杂 JSON 结构 { "server": { "host": "{server.host}", "port": {server.port}, "enabled": {server.enabled} } } ``` ### 3. 表达式语法(类 JavaScript) #### 3.1 变量访问 支持访问对象的属性: ``` {user.name} {user.age} {company.department.name} ``` #### 3.2 方法调用 支持调用对象的方法: ``` {user.getName()} {user.setName('张三')} {list.size()} {str.substring(0, 5)} ``` #### 3.3 字面量 EasyTL 支持多种类型的字面量,用于在表达式中直接表示常量值。 ##### 3.3.1 整数字面量 支持普通整数、长整数和大整数: **普通整数:** ``` {user.setAge(25)} {count + 100} {-42} ``` **长整数(L 结尾):** ``` {timestamp = 1234567890123L} {bigNumber = 999999999999999L} {-1000000000L} ``` **大整数(数值超出 long 范围时自动转换为 BigInteger):** ``` {veryBigNumber = 123456789012345678901234567890} {hugeValue = 999999999999999999999999999999} ``` **说明:** - 普通整数范围:-2,147,483,648 到 2,147,483,647(int 类型) - 长整数范围:-9,223,372,036,854,775,808 到 9,223,372,036,854,775,807(long 类型) - 大整数:超出 long 范围的整数自动使用 BigInteger 类型,支持任意精度 - 所有整数类型都支持负数,直接在数字前加负号即可 ##### 3.3.2 浮点数字面量 支持普通浮点数和大数(高精度小数): **普通浮点数:** ``` {product.setPrice(99.99)} {0.618} {3.14159} {-1.5} ``` **科学计数法:** ``` {1.23e10} {1.5e-8} {9.99E+20} ``` **大数(BigDecimal,用于高精度计算):** ``` {99.999999999999} {123456.789012345678} ``` **说明:** - 普通浮点数使用 double 类型,精度约 15-17 位有效数字 - 支持科学计数法表示法(e 或 E),如 1.23e10 表示 12,300,000,000 - 当小数位数超过 double 精度范围时,自动使用 BigDecimal 类型 - BigDecimal 类型支持任意精度的小数运算,适用于金融计算等场景 ##### 3.3.3 字符串字面量 支持单引号、双引号两种格式的字符串: **单引号字符串:** ``` {user.setName('张三')} {'Hello World'} {'It\'s a beautiful day'} // 转义单引号 ``` **双引号字符串:** ``` {user.setName("李四")} {"Hello World"} {"He said \"Hello\""} // 转义双引号 ``` **转义字符:** ``` {"第一行\n第二行"} // 换行符 {"列1\t列2\t列3"} // 制表符 {"路径:C:\\Windows"} // 反斜杠 {"引号:\"内容\""} // 双引号 {'单引号:\'内容\''} // 单引号 ``` **说明:** - 单引号和双引号字符串在功能上完全等价 - 字符串内可以使用反斜杠 `\` 进行转义 - 支持的转义字符:`\n`(换行)、`\t`(制表符)、`\\`(反斜杠)、`\'`(单引号)、`\"`(双引号) - 字符串可以包含 Unicode 字符和中文 ##### 3.3.4 字符串模板字面量 支持使用反引号包裹的字符串模板,可以在字符串中嵌入表达式: **基本语法:** ``` {greeting = `Hello {user.name}!`} {message = `您好,{name},欢迎来到 {city}`} ``` **嵌套表达式:** ``` {`总价:{price * quantity}元`} {`用户 {user.name} 的年龄是 {user.age} 岁`} {`订单状态:{order.status == 1 ? '已完成' : '进行中'}`} ``` **说明:** - 字符串模板使用反引号(`` ` ``)包裹 - 在模板中使用 `{表达式}` 嵌入动态内容 - 支持任意复杂的表达式,包括运算、方法调用、三元运算符等 - 字符串模板会在运行时计算所有嵌入的表达式,并将结果拼接成最终字符串 - 支持多行字符串,保留换行符和缩进 ##### 3.3.5 区间字面量 支持定义数值区间,常用于范围判断和迭代: **全闭区间(包含两端边界):** ``` {-100..100} // 包含 -100 和 100,范围是 [-100, 100] {0..10} // 包含 0 和 10,范围是 [0, 10] ``` **全开区间(不包含边界):** ``` {-100>..<100} // 不包含 -100 和 100,范围是 (-100, 100) {0>..<10} // 不包含 0 和 10,范围是 (0, 10) ``` **前闭后开区间(包含起始值,不包含结束值):** ``` {-100..<100} // 包含 -100,不包含 100,范围是 [-100, 100) {0..<10} // 包含 0,不包含 10,范围是 [0, 10) ``` **前开后闭区间(不包含起始值,包含结束值):** ``` {-100>..100} // 不包含 -100,包含 100,范围是 (-100, 100] {0>..10} // 不包含 0,包含 10,范围是 (0, 10] ``` **区间应用示例:** ``` // 判断数值是否在区间内 {score in 0>..<100 ? '有效分数' : '无效分数'} // 区间迭代(配合循环语句使用) {% for i in 1..10} 第 {i} 项 {/% for} // 区间作为参数传递 {list.subList(0>..5)} ``` **说明:** - 区间字面量用于表示一个连续的数值范围 - 四种区间类型对应数学中的开区间、闭区间概念 - `>` 符号表示不包含左边界,`..` 表示区间连接符,`<` 符号表示不包含右边界 - 全闭区间:`a..b` → [a, b] - 全开区间:`a>....b` → (a, b] - 区间常用于范围检查(in 运算符)和循环迭代 - 区间的起始值必须小于结束值 ##### 3.3.6 列表字面量 支持使用方括号定义列表(数组),列表中可以包含任意类型的表达式: **基本语法:** ``` [表达式1, 表达式2, 表达式3, ...] ``` **整数列表:** ``` {[1, 2, 3, 4, 5]} {[2, 3, 5, 7, 11, 13]} {[-10, -20, -30]} ``` **字符串列表:** ``` {['red', 'green', 'blue']} {["北京", "上海", "广州", "深圳"]} {['张三', "李四", '王五']} ``` **混合类型列表:** ``` {[1, 'hello', 3.14, true, null]} {[user.id, user.name, user.age]} ``` **表达式列表:** ``` {[a + b, c * d, sqrt(x)]} {[user.name, user.getName(), "hello " + user[1].name]} {[score * 0.9, score + bonus, maxScore - penalty]} ``` **嵌套列表:** ``` {[[1, 2, 3], [4, 5, 6], [7, 8, 9]]} {[['a', 'b'], ['c', 'd']]} {[1, [2, 3], [4, [5, 6]]]} ``` **空列表:** ``` {[]} ``` **列表应用示例:** ``` // 定义列表并遍历 {% let fruits = ['苹果', '香蕉', '橙子'] %} {% for fruit in fruits} - {fruit} {/% for} // 使用 in 运算符判断元素是否在列表中 {role in ['admin', 'moderator'] ? '管理员' : '普通用户'} // 列表作为方法参数 {processor.handleItems([item1, item2, item3])} // 访问列表元素 {% let numbers = [10, 20, 30, 40] %} 第一个数字:{numbers[0]} 最后一个数字:{numbers[3]} // 动态构建列表 {% let userInfo = [user.id, user.name, user.age, user.email] %} {userInfo[1]} 的年龄是 {userInfo[2]} 岁 ``` **说明:** - 列表字面量使用方括号 `[]` 包裹,元素之间用逗号 `,` 分隔 - 列表中可以包含任意类型的元素:整数、浮点数、字符串、布尔值、null 等 - 支持混合类型列表,同一个列表中可以包含不同类型的元素 - 列表中的元素可以是任意复杂的表达式,包括变量访问、方法调用、运算表达式等 - 支持嵌套列表,列表的元素可以是另一个列表 - 空列表用 `[]` 表示 - 列表在 Java 中会被转换为 `java.util.ArrayList` 类型 - 可以通过索引访问列表元素,索引从 0 开始:`list[0]`、`list[1]` 等 - 列表可以用于 `in` 运算符进行元素包含判断 - 列表可以用于 `for` 循环进行遍历 - 列表可以作为方法参数传递 ##### 3.3.7 哈希表字面量 支持使用花括号定义哈希表(Map),用于存储键值对: **基本语法:** ``` {键1: 表达式1, 键2: 表达式2, 键3: 表达式3, ...} ``` **标识符作为键:** ``` {% let user = {name: "张三", age: 25, city: "北京"} %} {% let config = {debug: true, timeout: 3000, retries: 3} %} {% let point = {x: 100, y: 200} %} ``` **字符串作为键:** ``` {% let map = {"a": 1, "b": 2, "c": 3} %} {% let settings = {"max-size": 1000, "min-value": 10} %} {% let data = {"user-name": "李四", "user-id": 12345} %} ``` **数字作为键:** ``` {% let statusMap = {0: "待处理", 1: "进行中", 2: "已完成"} %} {% let scoreMap = {90: "优秀", 80: "良好", 60: "及格"} %} ``` **混合类型的键:** ``` {% let mixed = { name: "混合示例", "string-key": "字符串键", 100: "数字键", active: true } %} ``` **值为表达式:** ``` {% let calculated = { sum: a + b, product: a * b, average: (a + b) / 2, message: `结果是 {a + b}` } %} ``` **嵌套哈希表:** ``` {% let nested = { user: {name: "张三", age: 25}, address: {city: "北京", street: "长安街"}, contact: {phone: "123456", email: "user@example.com"} } %} ``` **空哈希表:** ``` {% let empty = {} %} ``` **哈希表应用示例:** ``` // 定义用户信息 {% let user = { id: 1001, name: "张三", age: 28, email: "zhangsan@example.com", role: "admin" } %} 用户ID:{user.id} 用户名:{user.name} 年龄:{user.age} // 定义配置项 {% let config = { apiUrl: "https://api.example.com", timeout: 5000, retryCount: 3, enableCache: true } %} API地址:{config.apiUrl} 超时时间:{config.timeout}ms 重试次数:{config.retryCount} // 状态码映射 {% let statusMessages = { 200: "成功", 404: "未找到", 500: "服务器错误" } %} {% switch httpStatus } {% case 200 }{statusMessages[200]} {% case 404 }{statusMessages[404]} {% case 500 }{statusMessages[500]} {/% switch } // 使用方括号访问 {% let data = {"user-name": "李四", "user-id": 10086} %} 用户:{data["user-name"]},ID:{data["user-id"]} // 动态构建哈希表 {% let userMap = { name: user.getName(), age: user.getAge(), email: user.getEmail(), status: user.isActive() ? "激活" : "禁用" } %} ``` **说明:** - 哈希表字面量使用花括号 `{}` 包裹,键值对之间用逗号 `,` 分隔 - 每个键值对使用冒号 `:` 分隔键和值 - 键可以是标识符(如 `name`)、字符串(如 `"name"`)或数字(如 `0`) - 当键是标识符时,会被自动转换为字符串 - 值可以是任意类型的表达式:数字、字符串、布尔值、null、变量、运算表达式等 - 支持嵌套哈希表,哈希表的值可以是另一个哈希表 - 空哈希表用 `{}` 表示 - 哈希表在 Java 中会被转换为 `java.util.HashMap` 类型 - 可以使用点号访问标识符类型的键:`map.key` - 可以使用方括号访问任意类型的键:`map["key"]`、`map[0]` - 哈希表可以用于 `in` 运算符进行键存在判断 - 哈希表可以作为方法参数传递 #### 3.4 布尔值和空值 ``` {user.setActive(true)} {user.setDeleted(false)} {user.setAddress(null)} ``` #### 3.5 数组/列表访问 ``` {list[0]} {array[index]} {map['key']} ``` #### 3.6 算术运算 ``` {a + b} {a - b} {a * b} {a / b} {a % b} ``` #### 3.7 比较运算 ``` {a > b} {a < b} {a >= b} {a <= b} {a == b} {a != b} ``` #### 3.8 逻辑运算 ``` {a && b} {a || b} {!a} ``` #### 3.9 三元运算符 ``` {age >= 18 ? '成年' : '未成年'} {score >= 60 ? '及格' : '不及格'} ``` #### 3.10 链式调用 ``` {user.getName().toUpperCase()} {list.get(0).getAddress().getCity()} ``` #### 3.11 空安全操作符 支持使用 `?.` 进行空安全访问,当对象为 null 时,不会继续执行后续操作,直接返回 null: ``` {user?.name} {user?.address?.city} {list?.get(0)?.name} ``` **示例说明:** - 如果 `user` 为 null,`{user?.name}` 将返回 null,而不会抛出空指针异常 - 可以链式使用空安全操作符,如 `{user?.address?.city}` - 空安全操作符也适用于方法调用,如 `{user?.getName()?.toUpperCase()}` #### 3.12 Elvis 表达式 支持使用 `??` 运算符提供默认值,当左侧表达式为 null 或空时,返回右侧的默认值: ``` {name ?? "匿名用户"} {user.email ?? "未设置邮箱"} {score ?? 0} ``` **示例说明:** - 如果 `name` 为 null,`{name ?? "匿名用户"}` 将返回 "匿名用户" - 可以与空安全操作符结合使用:`{user?.name ?? "匿名用户"}` - Elvis 表达式的右侧可以是任意表达式:`{value ?? getDefaultValue()}` #### 3.13 正则表达式字面量 支持使用斜杠包裹的正则表达式字面量,语法与 JavaScript 类似: ``` {/[a-zA-Z]/} {/\d+/} {/\w+[\d\w]*/} {/[\d]+/gi} ``` **语法格式:** - 基本格式:`/pattern/` - 带标志位:`/pattern/flags` **支持的标志位:** - `g` - 全局匹配 - `i` - 忽略大小写 - `m` - 多行模式 **示例说明:** - `/[a-zA-Z]/` - 匹配任意单个字母 - `/\d+/` - 匹配一个或多个数字 - `/\w+/i` - 匹配一个或多个单词字符,忽略大小写 - `/^[\d]{3,6}$/` - 匹配3到6位数字 #### 3.14 正则匹配运算符 支持使用 `=~` 运算符进行正则表达式匹配,返回布尔值: ``` {name =~ /\w+[\d\w]*/} {email =~ /^[\w.-]+@[\w.-]+\.\w+$/} {phone =~ /^1[3-9]\d{9}$/} ``` **示例说明:** - 如果 `name` 匹配正则表达式 `/\w+[\d\w]*/`,返回 `true`,否则返回 `false` - 可以用于条件判断:`{email =~ /^[\w.-]+@/ ? '有效邮箱' : '无效邮箱'}` - 左侧必须是字符串类型,右侧必须是正则表达式字面量 #### 3.15 正则不匹配运算符 支持使用 `!=~` 运算符进行正则表达式不匹配判断,返回布尔值: ``` {name !=~ /[\d]+/} {username !=~ /^admin$/i} {password !=~ /^123456$/} ``` **示例说明:** - 如果 `name` 不匹配正则表达式 `/[\d]+/`,返回 `true`,否则返回 `false` - 等价于 `!(name =~ /[\d]+/)` - 可用于验证:`{username !=~ /^(admin|root)$/i ? '用户名可用' : '用户名被禁用'}` #### 3.16 条件表达式 支持使用 `if` 表达式根据条件返回不同的值,与条件语句块不同,条件表达式是一个可以作为值使用的表达式。 **语法格式:** ``` if (条件表达式) 语句块 if (条件表达式) 语句块1 else 语句块2 if (条件表达式1) 语句块1 else if (条件表达式2) 语句块2 else 语句块3 ``` **语句块格式:** ``` { 语句1; 语句2; 语句3 } ``` 或使用换行符分隔: ``` { 语句1 语句2 语句3 } ``` **返回值规则:** - 语句块的返回值为最后一条语句的返回值 - 条件表达式的返回值为被执行的语句块的返回值 **使用范围:** - 条件表达式只能在自闭合模板语句块中使用 - 不能在纯文本中直接使用 **重要规则:** - 当 if 表达式用于赋值(放在 `=` 右边)时,**必须包含 else 语句块**,否则编译报错 - 这是因为赋值语句需要确保变量一定会被赋予一个值 - 如果不需要 else 分支,请使用条件语句块 `{% if ... %}{/% if %}` 代替 **基本示例:** ``` // ✅ 正确:赋值表达式带有 else {% let result = if (a == 1) {"A"} else {"B"} %} {% let status = if (age >= 18) {"成年"} else {"未成年"} %} // ❌ 错误:赋值表达式缺少 else(编译报错) {% let result = if (a == 1) {"A"} %} // ✅ 正确:非赋值场景可以不带 else(仅执行副作用) {% if (debug) { log("debug mode") } %} ``` **多条语句示例:** ``` {% let name = if (score >= 90) { level = "优秀" color = "green" "A" } else if (score >= 60) { level = "及格" color = "blue" "B" } else { level = "不及格" color = "red" "C" } %} ``` **说明:** - 在上面的例子中,`name` 的值为 `"A"`、`"B"` 或 `"C"`(语句块最后一条语句的值) - `level` 和 `color` 变量会被设置到上下文中 - 条件表达式会根据条件选择执行对应的语句块 - 支持 `else if` 进行多条件判断 **应用示例:** ``` {% let greeting = if (hour < 12) {"早上好"} else if (hour < 18) {"下午好"} else {"晚上好"} %} 欢迎您,{greeting}! {% let message = if (status == 1) { log("订单待处理") "您的订单正在处理中" } else if (status == 2) { log("订单处理中") "订单处理中,请稍候" } else { log("订单已完成") "订单已完成" } %} {message} ``` **与三元运算符的区别:** - 三元运算符:`{age >= 18 ? '成年' : '未成年'}`,只能是简单的表达式 - 条件表达式:`if (age >= 18) {...} else {...}`,可以包含多条语句,支持 else if - 条件表达式更适合复杂的条件逻辑和需要执行多条语句的场景 **与条件语句块的区别:** - 条件语句块:`{% if ... }...{/% if }`,用于控制模板输出,是标准模板语句块 - 条件表达式:`if (...) {...}`,用于计算值,只能在自闭合模板语句块中使用 #### 3.17 in 表达式 支持使用 `in` 运算符判断元素是否在集合或区间中,返回布尔值: ##### 3.17.1 集合包含判断 判断变量是否在集合(数组、List、Set 等)中: ``` {a in numbers} {user.id in adminIds} {status in ['pending', 'processing', 'completed']} ``` **示例说明:** - 如果变量 `a` 在集合 `numbers` 中,返回 `true`,否则返回 `false` - 支持任何实现了 `Collection` 接口的集合类型 - 右侧可以是变量引用的集合,也可以是数组字面量 **应用示例:** ``` {role in ['admin', 'moderator'] ? '管理员权限' : '普通用户'} {userId in blacklist ? '已被封禁' : '正常用户'} {color in ['red', 'green', 'blue'] ? '有效颜色' : '无效颜色'} ``` ##### 3.17.2 区间包含判断 判断数值是否在指定区间内: ``` {num in 0..100} {age in 18>..<65} {score in 60>..100} {temperature in -10..<40} ``` **示例说明:** - 如果变量 `num` 在区间 `0..100` 中(包含边界),返回 `true`,否则返回 `false` - 支持所有区间类型:全闭区间 `a..b`、全开区间 `a>....b` - 区间边界支持整数和浮点数 - 左侧必须是数值类型 **应用示例:** ``` {score in 0>..<100 ? '有效分数' : '无效分数'} {age in 18>..<60 ? '适龄工作者' : '不在工作年龄范围'} {temperature in -10>..<35 ? '正常温度' : '温度异常'} {price in 0>..1000 ? '价格合理' : '价格超出范围'} ``` ##### 3.17.3 Map 键包含判断 判断键是否存在于 Map 中: ``` {key in userMap} {'username' in config} {productId in inventory} ``` **示例说明:** - 如果键 `key` 存在于 Map `userMap` 中,返回 `true`,否则返回 `false` - 支持任何实现了 `Map` 接口的映射类型 - 可用于判断配置项是否存在 **应用示例:** ``` {'debug' in config ? config.get('debug') : false} {userId in userCache ? '缓存命中' : '缓存未命中'} ``` ##### 3.17.4 字符串包含判断 判断子字符串是否包含在字符串中: ``` {'admin' in username} {'@' in email} {keyword in content} ``` **示例说明:** - 如果子字符串 `'admin'` 包含在字符串 `username` 中,返回 `true`,否则返回 `false` - 字符串匹配区分大小写 - 等价于 Java 的 `String.contains()` 方法 **应用示例:** ``` {'@' in email ? '邮箱格式可能正确' : '邮箱格式错误'} {'test' in username ? '测试账号' : '正式账号'} {keyword in article.content ? '包含关键词' : '不包含关键词'} ``` ##### 3.17.5 !in 表达式(不包含判断) 支持使用 `!in` 运算符判断元素是否不在集合、区间、Map 或字符串中,返回布尔值。`!in` 是 `in` 的否定形式。 **语法格式:** ``` {变量 !in 集合/区间/Map/字符串} ``` **集合不包含判断:** ``` {a !in numbers} {user.id !in blacklist} {status !in ['deleted', 'banned', 'suspended']} ``` **示例说明:** - 如果变量 `a` 不在集合 `numbers` 中,返回 `true`,否则返回 `false` - 等价于 `!(a in numbers)` - 适用于黑名单、排除列表等场景 **区间不包含判断:** ``` {age !in 0>..18} {score !in 60>..100} {temperature !in -10>..<40} ``` **示例说明:** - 如果数值不在指定区间内,返回 `true`,否则返回 `false` - 支持所有区间类型 **Map 键不包含判断:** ``` {key !in userMap} {'admin' !in permissions} {productId !in inventory} ``` **示例说明:** - 如果键不存在于 Map 中,返回 `true`,否则返回 `false` - 可用于判断配置项是否缺失 **字符串不包含判断:** ``` {'admin' !in username} {'@' !in text} {keyword !in content} ``` **示例说明:** - 如果子字符串不包含在字符串中,返回 `true`,否则返回 `false` - 等价于 Java 的 `!String.contains()` 方法 **应用示例:** ``` {userId !in blacklist ? '正常用户' : '已被封禁'} {age !in 0>..18 ? '成年人' : '未成年人'} {'test' !in username ? '正式账号' : '测试账号'} {'debug' !in config ? '生产环境' : '调试模式'} {score !in 0>..60 ? '及格' : '不及格'} ``` #### 3.18 for 循环语句 支持使用 `for` 循环语句在自闭合模板语句块中遍历集合或区间,执行副作用操作。 **语法格式:** ``` for (变量 in 表达式) { 语句块 } for (索引变量, 值变量 in 表达式) { 语句块 } ``` **使用范围:** - for 循环语句只能在自闭合模板语句块中使用 - 不能在纯文本中直接使用 **与循环语句块的区别:** - 循环语句块:`{% for ... }...{/% for }`,用于控制模板输出,是标准模板语句块 - for 循环语句:`for (...) {...}`,用于执行副作用操作,只能在自闭合模板语句块中使用 **基本语法示例:** ``` // 单变量循环 {% for (user in userList) { names.add(user.name) } %} // 带索引的循环 {% for (i, user in userList) { user.setIndex(i) processedList.add(user) } %} // 遍历区间 {% for (i in 1..10) { sum = sum + i } %} ``` **语句块格式:** ``` { 语句1 语句2 语句3 } ``` 或使用分号分隔: ``` { 语句1; 语句2; 语句3 } ``` **返回值规则:** - for 循环语句执行完成后返回 null - 循环体内的语句按顺序执行 - 循环变量在循环结束后不可访问 **完整示例:** ``` {% let names = [] let scores = [] for (user in userList) { names.add(user.name) scores.add(user.score) } %} 用户数量:{names.size()} ``` **带索引的循环示例:** ``` {% let orderedList = [] for (index, item in items) { item.setOrder(index + 1) orderedList.add(item) } %} ``` **遍历区间示例:** ``` {% let sum = 0 for (i in 1..100) { sum = sum + i } %} 总和:{sum} ``` **嵌套循环示例:** ``` {% let matrix = [] for (i in 1..3) { let row = [] for (j in 1..3) { row.add(i * j) } matrix.add(row) } %} ``` **应用示例:** ``` {% // 数据预处理 let validUsers = [] for (user in allUsers) { if (user.age >= 18) { validUsers.add(user) } } // 数据统计 let totalScore = 0 for (i, user in validUsers) { totalScore = totalScore + user.score user.setRank(i + 1) } let avgScore = totalScore / validUsers.size() %} 有效用户数:{validUsers.size()} 平均分数:{avgScore} ``` **说明:** - for 循环语句主要用于执行副作用操作,如修改对象、填充列表等 - 循环变量的作用域仅限于循环体内 - 支持遍历任何实现了 `Iterable` 接口的集合 - 支持遍历区间字面量 - 循环体内可以使用 let 声明变量,这些变量在循环外部也可访问 - 与 if 表达式类似,for 循环语句也是一种脚本语句,不是模板控制语句 ### 4. 模板语句块 模板引擎支持两种类型的模板语句块,用于在模板中嵌入控制逻辑和脚本代码,以区别于表达式内部使用的普通语句块: #### 4.1 标准模板语句块(有结束语句) 标准模板语句块用于流程控制,在开始语句和结束语句之间可以插入普通文本和其他模板语句块。 **语法格式:** ``` {% 语句名称 语句参数 } 文本内容和其他模板语句块 {/% 语句名称 } ``` **语法说明:** - 开始语句:以 `{%` 开始,以 `}` 结束 - 结束语句:以 `{/%` 开始,以 `}` 结束 - 结束语句的名称必须与开始语句的名称一致 - 标准模板语句块可以嵌套使用 - 模板语句块内部可以包含普通文本、表达式以及其他模板语句块 **适用场景:** - 流程控制语句(if-else、for、switch) - 宏调用 - 自定义命令 **示例:** ``` {% upper }hello world{/% upper } ``` 可能输出:`HELLO WORLD`(假设 upper 命令将文本转为大写) #### 4.2 自闭合模板语句块(无结束语句) 自闭合模板语句块用于嵌入脚本代码,不需要结束语句,也不能在中间插入文本和其他模板语句块。 **语法格式:** 单条语句: ``` {% 语句 %} ``` 多条语句(用换行或分号分隔): ``` {% 语句1 语句2 语句3 %} ``` 或者: ``` {% 语句1; 语句2; 语句3 %} ``` **语法说明:** - 以 `{%` 开始,以 `%}` 结束 - 可以包含一条或多条语句 - 多条语句之间用换行符或分号分隔 - 不能在模板语句块中间插入文本 **适用场景:** - 变量声明和赋值(let) - 模板扩展(extends) - Java 类导入(import) - 其他单行或多行脚本代码 **示例:** 单条语句: ``` {% let name = 'EasyTL' %} ``` 多条语句: ``` {% let a = 1 let b = 2 let c = a + b %} ``` ### 5. 条件语句块 支持在模板中使用条件语句块来根据条件动态输出内容。条件语句块是标准模板语句块,有开始和结束标签。 #### 5.1 基本 if 语句 **语法格式:** ``` {% if 条件表达式 } 文本内容 {/% if } ``` **示例:** ``` {% if flag == 1 }文本1{/% if } 文本2 ``` **说明:** - 当条件表达式 `flag == 1` 为真时,输出 "文本1文本2" - 当条件表达式为假时,只输出 "文本2" - 条件表达式支持所有表达式语法,包括比较运算、逻辑运算等 - 条件语句块内部可以包含普通文本、表达式以及其他模板语句块 #### 5.2 if-else 语句 **语法格式:** ``` {% if 条件表达式 } 文本1 {% else } 文本2 {/% if } ``` **示例:** ``` {% if flag == 1 }文本1{% else }文本2{/% if } 文本3 ``` **说明:** - 当 `flag == 1` 为真时,输出 "文本1文本3" - 当 `flag == 1` 为假时,输出 "文本2文本3" - `else` 块是可选的 #### 5.3 if-else if-else 语句 **语法格式:** ``` {% if 条件表达式1 } 文本1 {% else if 条件表达式2 } 文本2 {% else if 条件表达式3 } 文本3 {% else } 文本4 {/% if } ``` **示例:** ``` {% if flag == 1 }文本1{% else if flag == 2 }文本2{% else if flag == 3 }文本3{% else }文本4{/% if } 文本5 ``` **说明:** - 当 `flag == 1` 时,输出 "文本1文本5" - 当 `flag == 2` 时,输出 "文本2文本5" - 当 `flag == 3` 时,输出 "文本3文本5" - 当以上条件都不满足时,输出 "文本4文本5" - 可以有多个 `else if` 分支 - `else` 分支是可选的,如果没有 `else` 分支且所有条件都不满足,则不输出任何内容 **完整示例:** ``` 订单状态: {% if order.status == 'pending' }待处理{/% if } {% if order.status == 'processing' }处理中{/% if } {% if order.status == 'completed' }已完成{/% if } 用户等级: {% if user.score >= 1000 }钻石会员 {% else if user.score >= 500 }黄金会员 {% else if user.score >= 100 }白银会员 {% else }普通会员 {/% if } ``` ### 6. 循环语句块 支持在模板中使用循环语句块来遍历集合或区间,动态生成重复内容。循环语句块是标准模板语句块,有开始和结束标签。 #### 6.1 基本 for 循环 **语法格式:** ``` {% for 变量 in 集合 } 文本内容 {/% for } ``` **示例:** ``` {% for i in numbers } i = {i} {/% for } ``` **说明:** - `numbers` 是一个集合(数组、List 等)或区间 - `i` 是循环变量,每次迭代时会被赋值为集合中的当前元素 - 循环体内可以使用表达式 `{i}` 来访问当前循环变量的值 - 循环体内可以包含普通文本、表达式以及其他模板语句块 **示例应用:** ``` 用户列表: {% for user in userList } - 用户名:{user.name},年龄:{user.age} {/% for } ``` 假设 `userList` 包含三个用户,可能输出: ``` 用户列表: - 用户名:张三,年龄:25 - 用户名:李四,年龄:30 - 用户名:王五,年龄:28 ``` #### 6.2 带索引的 for 循环 **语法格式:** ``` {% for 索引变量, 值变量 in 集合 } 文本内容 {/% for } ``` **示例:** ``` {% for i, user in userList } 第{i}个用户名字叫:{user.name} {/% for } ``` **说明:** - 使用两个参数时,第一个参数 `i` 为索引值(从 0 开始) - 第二个参数 `user` 为当前循环到的值 - 索引和值都可以在循环体内使用 - 适用于需要同时获取元素位置和元素值的场景 **示例应用:** ``` 排行榜: {% for index, player in rankList } 第 {index + 1} 名:{player.name},得分:{player.score} {/% for } ``` 假设 `rankList` 包含三个玩家,可能输出: ``` 排行榜: 第 1 名:玩家A,得分:9500 第 2 名:玩家B,得分:8800 第 3 名:玩家C,得分:7600 ``` #### 6.3 区间循环 **语法格式:** ``` {% for i in 起始值>..结束值 } 文本内容 {/% for } ``` **示例:** ``` {% for i in 1>..<10 } 第 {i} 项 {/% for } ``` **说明:** - 支持使用区间字面量进行数值范围的循环 - 区间类型包括:全闭区间 `a..b`、全开区间 `a>....b` - 循环变量 `i` 会从起始值遍历到结束值(根据区间类型决定是否包含边界) - 适用于需要生成固定次数的重复内容 **示例应用:** ``` 九九乘法表: {% for i in 1>..<9 } {% for j in 1>..= 18 ? '成年人' : '未成年人'}"); Context context = new Context(); context.put("age", 20); String result = template.render(context); // 输出:状态:成年人 ``` ### 示例 5:复杂表达式 ```java TemplateEngine engine = new TemplateEngine(); Template template = engine.compile( "订单总价:{order.getPrice() * order.getQuantity()}元," + "折扣后:{order.getPrice() * order.getQuantity() * 0.9}元" ); Order order = new Order(); order.setPrice(100.0); order.setQuantity(3); Context context = new Context(); context.put("order", order); String result = template.render(context); // 输出:订单总价:300.0元,折扣后:270.0元 ``` ### 示例 6:空安全操作符 ```java TemplateEngine engine = new TemplateEngine(); Template template = engine.compile("用户地址:{user?.address?.city}"); Context context = new Context(); // user 为 null 的情况 context.put("user", null); String result = template.render(context); // 输出:用户地址:null // user 不为 null,但 address 为 null 的情况 User user = new User(); user.setAddress(null); context.put("user", user); result = template.render(context); // 输出:用户地址:null // user 和 address 都不为 null 的情况 Address address = new Address(); address.setCity("上海"); user.setAddress(address); result = template.render(context); // 输出:用户地址:上海 ``` ### 示例 7:Elvis 表达式 ```java TemplateEngine engine = new TemplateEngine(); Template template = engine.compile("欢迎您,{name ?? '匿名用户'}!"); Context context = new Context(); // name 为 null 的情况 context.put("name", null); String result = template.render(context); // 输出:欢迎您,匿名用户! // name 有值的情况 context.put("name", "张三"); result = template.render(context); // 输出:欢迎您,张三! ``` ### 示例 8:空安全与 Elvis 表达式组合 ```java TemplateEngine engine = new TemplateEngine(); Template template = engine.compile( "用户信息:{user?.name ?? '匿名用户'}," + "邮箱:{user?.email ?? '未设置'}," + "城市:{user?.address?.city ?? '未知'}" ); Context context = new Context(); // user 为 null 的情况 context.put("user", null); String result = template.render(context); // 输出:用户信息:匿名用户,邮箱:未设置,城市:未知 // user 有部分信息的情况 User user = new User(); user.setName("李四"); user.setEmail(null); user.setAddress(null); context.put("user", user); result = template.render(context); // 输出:用户信息:李四,邮箱:未设置,城市:未知 ``` ### 示例 9:正则表达式验证 ```java TemplateEngine engine = new TemplateEngine(); // 邮箱验证 Template emailTemplate = engine.compile( "邮箱格式:{email =~ /^[\\w.-]+@[\\w.-]+\\.\\w+$/ ? '有效' : '无效'}" ); Context context = new Context(); context.put("email", "user@example.com"); String result = emailTemplate.render(context); // 输出:邮箱格式:有效 context.put("email", "invalid-email"); result = emailTemplate.render(context); // 输出:邮箱格式:无效 // 手机号验证 Template phoneTemplate = engine.compile( "手机号:{phone =~ /^1[3-9]\\d{9}$/ ? '正确' : '错误'}" ); context.put("phone", "13812345678"); result = phoneTemplate.render(context); // 输出:手机号:正确 // 用户名验证(不能是纯数字) Template usernameTemplate = engine.compile( "用户名:{username !=~ /^\\d+$/ ? '可用' : '不可用(不能为纯数字)'}" ); context.put("username", "user123"); result = usernameTemplate.render(context); // 输出:用户名:可用 context.put("username", "123456"); result = usernameTemplate.render(context); // 输出:用户名:不可用(不能为纯数字) ``` ### 示例 10:正则表达式组合验证 ```java TemplateEngine engine = new TemplateEngine(); Template template = engine.compile( "密码强度:" + "{password =~ /^.{8,}$/ && " + " password =~ /[A-Z]/ && " + " password =~ /[a-z]/ && " + " password =~ /\\d/ ? '强' : '弱'}" ); Context context = new Context(); // 强密码 context.put("password", "Abc12345"); String result = template.render(context); // 输出:密码强度:强 // 弱密码 context.put("password", "abc123"); result = template.render(context); // 输出:密码强度:弱 // 禁止特定用户名 Template userCheckTemplate = engine.compile( "注册结果:" + "{username !=~ /^(admin|root|system)$/i ? '注册成功' : '该用户名已被保留'}" ); context.put("username", "admin"); result = userCheckTemplate.render(context); // 输出:注册结果:该用户名已被保留 context.put("username", "john"); result = userCheckTemplate.render(context); // 输出:注册结果:注册成功 ``` ## 错误处理 ### 语法错误 当模板语法错误时,编译阶段会抛出 `TemplateSyntaxException`: ```java try { Template template = engine.compile("Hello {user.name"); // 缺少右花括号 } catch (TemplateSyntaxException e) { System.err.println("模板语法错误:" + e.getMessage()); } ``` ### 运行时错误 当表达式执行出错时,渲染阶段会抛出 `TemplateRuntimeException`: ```java try { String result = template.render(context); } catch (TemplateRuntimeException e) { System.err.println("模板执行错误:" + e.getMessage()); } ``` ## 性能特点 - **编译一次,多次渲染**:模板编译后可以重复使用,提高性能 - **无反射优化**:核心表达式执行尽量减少反射调用 - **内存友好**:使用对象池减少对象创建开销 - **线程安全**:Template 对象线程安全,可以在多线程环境下共享 ## 设计原则 1. **零依赖**:不依赖任何第三方库,保持轻量级 2. **简单易用**:API 设计简洁,学习成本低 3. **性能优先**:编译后的模板执行效率高 4. **安全可控**:支持沙箱模式,限制危险操作 5. **可扩展性**:支持自定义函数和运算符扩展 ## Maven 依赖 ```xml com.github.easytl easy-tl 1.0.0 ``` ## 项目结构 ``` easy-tl/ ├── src/ │ ├── main/ │ │ └── java/ │ │ └── com/ │ │ └── github/ │ │ └── easytl/ │ │ ├── TemplateEngine.java # 模板引擎主类 │ │ ├── Template.java # 模板接口 │ │ ├── Context.java # 上下文类 │ │ ├── exception/ # 异常类 │ │ ├── parser/ # 词法和语法解析器 │ │ ├── ast/ # 抽象语法树节点 │ │ ├── compiler/ # 编译器 │ │ └── runtime/ # 运行时执行器 │ └── test/ │ └── java/ │ └── com/ │ └── github/ │ └── easytl/ │ └── ... # 单元测试 ├── pom.xml ├── README.md └── PLAN.md ``` ## 许可证 MIT License ## 联系方式 如有问题或建议,欢迎提 Issue 或 PR。