# 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。