word-template-engine 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
PKG-INFO ADDED
@@ -0,0 +1,262 @@
1
+ Metadata-Version: 2.4
2
+ Name: word-template-engine
3
+ Version: 0.1.0
4
+ Summary: A Word template engine similar to docx-template, implemented with python-docx and lxml.
5
+ Project-URL: Homepage, https://github.com/yourname/word-template-engine
6
+ Project-URL: Repository, https://github.com/yourname/word-template-engine
7
+ Author-email: Your Name <you@example.com>
8
+ License: MIT
9
+ Requires-Python: >=3.8
10
+ Requires-Dist: lxml>=4.9.0
11
+ Requires-Dist: python-docx>=1.1.0
12
+ Requires-Dist: requests>=2.28.0
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Word模板引擎
16
+
17
+ 一个类似于docx-template的Word模板引擎,使用Python直接解析Word文档的XML结构实现,**完全保留所有样式信息**。
18
+
19
+ ## 功能特性
20
+
21
+ 1. **字符串文本替换**: `{{data.diagnose}}` - 替换为对应的数据值(**不需要结束标签**)
22
+
23
+ 2. **表格行循环**: `{{#data.mutationSiteZero}}` - 在表格头部标记,根据数据循环生成表格行,使用`[field_name]`作为字段占位符(**不需要结束标签**)
24
+
25
+ 3. **表格列循环**: `{{$data.mutationSiteZero}}` - 在表格头部标记,根据数据循环生成表格列(**不需要结束标签**)
26
+
27
+ 4. **条件区块**: `{{?data.field != null && data.field.size() > 0}}...{{/}}` - 根据条件显示或隐藏内容块(**需要结束标签 {{/}}**)
28
+
29
+ 5. **图片插入**: `{{@var}}` - 插入图片,var格式: `{'url': '', 'conf': {'w': '180px', 'h': '180px'}}`(**不需要结束标签**)
30
+
31
+ 6. **列表循环**: `{{*data.conclusion_C}}` - 循环显示列表项(**不需要结束标签**)
32
+
33
+ ## 标签说明
34
+
35
+ ### 有结束标签的标签
36
+
37
+ - **区块**: `{{?condition}}...{{/}}`
38
+ - 只有区块需要结束标签
39
+ - 支持嵌套
40
+ - 示例:
41
+ ```
42
+ {{?data.conclusion_C == ''}}无{{/}}
43
+ {{?data.conclusion_C != ''}}
44
+ {{*data.conclusion_C}}
45
+ {{/}}
46
+ ```
47
+
48
+ ### 不需要结束标签的标签
49
+
50
+ - **文本替换**: `{{data.field}}`
51
+ - **表格行循环**: `{{#data.field}}` - 后面直接跟字段如`[gene_transcript]`
52
+ - **表格列循环**: `{{$data.field}}`
53
+ - **图片**: `{{@var}}`
54
+ - **列表**: `{{*data.field}}`
55
+
56
+ ## 安装
57
+
58
+ 使用pip安装依赖:
59
+
60
+ ```bash
61
+ pip install wps_temp_engine
62
+ ```
63
+
64
+ **注意**: 本引擎直接解析Word文档的XML结构,这样可以完整保留所有样式信息。
65
+
66
+ ## 使用方法
67
+
68
+ ### 1. 创建Word模板
69
+
70
+ 在Word文档中使用以下占位符:
71
+
72
+ #### 文本替换
73
+ - `{{data.diagnose}}`
74
+
75
+ #### 表格行循环
76
+ 在表格行的单元格中写入 `{{#data.mutationSiteZero}}`,其他单元格使用方括号占位符:
77
+ - `[gene_transcript]`
78
+ - `[value]`
79
+ - `[description]`
80
+
81
+ #### 表格列循环
82
+ 在表格列的第一个单元格写入 `{{$data.mutationSiteZero}}`
83
+
84
+ #### 条件区块
85
+ ```text
86
+ {{?data.mutationSiteZero.size() > 0}}
87
+ 这部分内容只有在条件为真时才会显示
88
+ {{/}}
89
+ ```
90
+
91
+ 支持的条件:
92
+ - `size() > 0`
93
+
94
+ #### 图片
95
+ 方式1: 使用变量名
96
+ ```
97
+ {{@data.image_config}}
98
+ ```
99
+
100
+ 方式2: 直接写JSON
101
+ ```
102
+ {{@{'url':'path/to/image.jpg','conf':{'w':'180px','h':'180px'}}}}
103
+ ```
104
+
105
+ 支持的尺寸单位: px, cm, in, mm
106
+
107
+ #### 列表
108
+ ```
109
+ {{*data.conclusion_C}}
110
+ ```
111
+
112
+ ### 2. 使用Python代码渲染
113
+
114
+ ```python
115
+ from word_template_engine import WordTemplateEngine
116
+
117
+ # 创建引擎实例
118
+ engine = WordTemplateEngine('template.docx')
119
+
120
+ # 准备数据
121
+ data = {
122
+ 'data': {
123
+ 'diagnose': '诊断结果',
124
+ 'mutationSiteZero': [
125
+ {'gene_transcript': 'Gene1', 'value': 'Value1', 'description': 'Desc1'},
126
+ {'gene_transcript': 'Gene2', 'value': 'Value2', 'description': 'Desc2'},
127
+ ],
128
+ 'conclusion_C': ['结论1', '结论2', '结论3'],
129
+ }
130
+ }
131
+
132
+ # 渲染并保存
133
+ engine.render(data, 'output.docx')
134
+ ```
135
+
136
+ ## 详细说明
137
+
138
+ ### 1. 字符串文本替换
139
+
140
+ 模板中写入: `{{data.diagnose}}`
141
+
142
+ 数据中提供: `{'data': {'diagnose': '测试结果'}}`
143
+
144
+ 结果: 替换为 "测试结果"
145
+
146
+ ### 2. 表格行循环
147
+
148
+ 在表格的某一行的单元格中写入 `{{#data.mutationSiteZero}}`,该行将成为模板行。
149
+
150
+ 其他单元格使用方括号占位符,如 `[gene_transcript]`、`[value]`。
151
+
152
+ 根据 `data.mutationSiteZero` 数组的长度,会自动生成对应数量的表格行。
153
+
154
+ **注意**: 表格行循环**不需要结束标签**,循环标记后直接跟字段占位符。
155
+
156
+ ### 3. 表格列循环
157
+
158
+ 在表格某一列的单元格中写入 `{{$data.mutationSiteZero}}`,该列将成为模板列。
159
+
160
+ 根据数据数组长度,会自动为所有行添加新列。
161
+
162
+ ### 4. 条件区块
163
+
164
+ 支持条件判断,**需要结束标签**:
165
+
166
+ ```text
167
+ {{?data.conclusion_C == ''}}无{{/}}
168
+ {{?data.conclusion_C != ''}}
169
+ {{*data.conclusion_C}}
170
+ {{/}}
171
+ ```
172
+
173
+ 支持的操作符:
174
+ - `!= null` / `== null`
175
+ - `== ''` / `!= ''` (检查空字符串)
176
+ - `size() > 0`
177
+ - `&&` (AND)
178
+ - `||` (OR)
179
+
180
+ 支持嵌套区块。
181
+
182
+ ### 5. 图片插入
183
+
184
+ 两种方式:
185
+
186
+ 方式1: 使用变量名
187
+ ```
188
+ {{@data.image_config}}
189
+ ```
190
+ 其中 `data.image_config` 是字典格式的配置。
191
+
192
+ 方式2: 直接写JSON
193
+ ```
194
+ {{@{'url':'https://example.com/image.jpg','conf':{'w':'180px','h':'180px'}}}}
195
+ ```
196
+
197
+ 支持的尺寸单位: px, cm, in, mm
198
+
199
+ 图片可以是HTTP/HTTPS链接或本地文件路径。
200
+
201
+ ### 6. 列表循环
202
+
203
+ 模板中写入: `{{*data.conclusion_C}}`
204
+
205
+ 数据中提供: `{'data': {'conclusion_C': ['项目1', '项目2', '项目3']}}`
206
+
207
+ 结果: 展开为多行文本,每行一个项目
208
+
209
+ ## 处理顺序
210
+
211
+ 引擎按以下顺序处理模板:
212
+
213
+ 1. **区块** - 先处理条件区块(因为它有结束标签,可能包含其他标签)
214
+ 2. **列表** - 处理列表循环
215
+ 3. **文本** - 处理文本占位符
216
+ 4. **图片** - 处理图片占位符
217
+ 5. **表格** - 最后处理表格(行循环和列循环)
218
+
219
+ ## 项目结构
220
+
221
+ ```
222
+ .
223
+ ├── pyproject.toml # 项目配置
224
+ ├── word_template_engine.py # 主引擎类(XML版本)
225
+ ├── example.py # 使用示例
226
+ ├── README.md # 详细文档
227
+ ├── template.docx # 模板文件
228
+ └── utils/ # 工具模块
229
+ ├── __init__.py
230
+ ├── xml_processor.py # XML处理工具
231
+ ├── text_processor.py # 文本处理
232
+ ├── table_processor.py # 表格处理(保留兼容)
233
+ ├── block_processor.py # 区块处理
234
+ ├── image_processor.py # 图片处理
235
+ └── list_processor.py # 列表处理
236
+ ```
237
+
238
+ ## 技术实现
239
+
240
+ 本引擎直接解析Word文档的XML结构(.docx文件实际上是一个ZIP压缩包,包含XML文件),这样可以:
241
+
242
+ - **完全保留样式信息**:所有格式、字体、颜色、对齐等样式都会保留
243
+ - **精确控制**:直接操作XML节点,可以精确处理复杂的文档结构
244
+ - **高性能**:无需通过中间库转换,直接操作XML
245
+
246
+ 主要处理流程:
247
+ 1. 解压.docx文件(ZIP格式)
248
+ 2. 解析 `word/document.xml` 主文档
249
+ 3. 在XML层面进行占位符替换和循环处理
250
+ 4. 重新打包为.docx文件
251
+
252
+ ## 注意事项
253
+
254
+ 1. 模板文件必须是有效的.docx格式
255
+ 2. 表格行循环标记需要放在表格的单元格中
256
+ 3. 表格行循环**不需要结束标签**,直接在标记后使用`[field]`占位符
257
+ 4. 只有**区块**需要结束标签 `{{/}}`
258
+ 5. 图片URL可以是HTTP/HTTPS链接或本地文件路径(图片功能在XML版本中暂未完整实现)
259
+ 6. 条件表达式支持基本的逻辑运算
260
+ 7. 嵌套的字段访问使用点号,如 `data.sub_field.value`
261
+ 8. 支持嵌套的条件区块
262
+ 9. **所有样式都会完整保留**,包括字体、颜色、大小、对齐等,这是使用XML解析的最大优势
README.md ADDED
@@ -0,0 +1,248 @@
1
+ # Word模板引擎
2
+
3
+ 一个类似于docx-template的Word模板引擎,使用Python直接解析Word文档的XML结构实现,**完全保留所有样式信息**。
4
+
5
+ ## 功能特性
6
+
7
+ 1. **字符串文本替换**: `{{data.diagnose}}` - 替换为对应的数据值(**不需要结束标签**)
8
+
9
+ 2. **表格行循环**: `{{#data.mutationSiteZero}}` - 在表格头部标记,根据数据循环生成表格行,使用`[field_name]`作为字段占位符(**不需要结束标签**)
10
+
11
+ 3. **表格列循环**: `{{$data.mutationSiteZero}}` - 在表格头部标记,根据数据循环生成表格列(**不需要结束标签**)
12
+
13
+ 4. **条件区块**: `{{?data.field != null && data.field.size() > 0}}...{{/}}` - 根据条件显示或隐藏内容块(**需要结束标签 {{/}}**)
14
+
15
+ 5. **图片插入**: `{{@var}}` - 插入图片,var格式: `{'url': '', 'conf': {'w': '180px', 'h': '180px'}}`(**不需要结束标签**)
16
+
17
+ 6. **列表循环**: `{{*data.conclusion_C}}` - 循环显示列表项(**不需要结束标签**)
18
+
19
+ ## 标签说明
20
+
21
+ ### 有结束标签的标签
22
+
23
+ - **区块**: `{{?condition}}...{{/}}`
24
+ - 只有区块需要结束标签
25
+ - 支持嵌套
26
+ - 示例:
27
+ ```
28
+ {{?data.conclusion_C == ''}}无{{/}}
29
+ {{?data.conclusion_C != ''}}
30
+ {{*data.conclusion_C}}
31
+ {{/}}
32
+ ```
33
+
34
+ ### 不需要结束标签的标签
35
+
36
+ - **文本替换**: `{{data.field}}`
37
+ - **表格行循环**: `{{#data.field}}` - 后面直接跟字段如`[gene_transcript]`
38
+ - **表格列循环**: `{{$data.field}}`
39
+ - **图片**: `{{@var}}`
40
+ - **列表**: `{{*data.field}}`
41
+
42
+ ## 安装
43
+
44
+ 使用pip安装依赖:
45
+
46
+ ```bash
47
+ pip install wps_temp_engine
48
+ ```
49
+
50
+ **注意**: 本引擎直接解析Word文档的XML结构,这样可以完整保留所有样式信息。
51
+
52
+ ## 使用方法
53
+
54
+ ### 1. 创建Word模板
55
+
56
+ 在Word文档中使用以下占位符:
57
+
58
+ #### 文本替换
59
+ - `{{data.diagnose}}`
60
+
61
+ #### 表格行循环
62
+ 在表格行的单元格中写入 `{{#data.mutationSiteZero}}`,其他单元格使用方括号占位符:
63
+ - `[gene_transcript]`
64
+ - `[value]`
65
+ - `[description]`
66
+
67
+ #### 表格列循环
68
+ 在表格列的第一个单元格写入 `{{$data.mutationSiteZero}}`
69
+
70
+ #### 条件区块
71
+ ```text
72
+ {{?data.mutationSiteZero.size() > 0}}
73
+ 这部分内容只有在条件为真时才会显示
74
+ {{/}}
75
+ ```
76
+
77
+ 支持的条件:
78
+ - `size() > 0`
79
+
80
+ #### 图片
81
+ 方式1: 使用变量名
82
+ ```
83
+ {{@data.image_config}}
84
+ ```
85
+
86
+ 方式2: 直接写JSON
87
+ ```
88
+ {{@{'url':'path/to/image.jpg','conf':{'w':'180px','h':'180px'}}}}
89
+ ```
90
+
91
+ 支持的尺寸单位: px, cm, in, mm
92
+
93
+ #### 列表
94
+ ```
95
+ {{*data.conclusion_C}}
96
+ ```
97
+
98
+ ### 2. 使用Python代码渲染
99
+
100
+ ```python
101
+ from word_template_engine import WordTemplateEngine
102
+
103
+ # 创建引擎实例
104
+ engine = WordTemplateEngine('template.docx')
105
+
106
+ # 准备数据
107
+ data = {
108
+ 'data': {
109
+ 'diagnose': '诊断结果',
110
+ 'mutationSiteZero': [
111
+ {'gene_transcript': 'Gene1', 'value': 'Value1', 'description': 'Desc1'},
112
+ {'gene_transcript': 'Gene2', 'value': 'Value2', 'description': 'Desc2'},
113
+ ],
114
+ 'conclusion_C': ['结论1', '结论2', '结论3'],
115
+ }
116
+ }
117
+
118
+ # 渲染并保存
119
+ engine.render(data, 'output.docx')
120
+ ```
121
+
122
+ ## 详细说明
123
+
124
+ ### 1. 字符串文本替换
125
+
126
+ 模板中写入: `{{data.diagnose}}`
127
+
128
+ 数据中提供: `{'data': {'diagnose': '测试结果'}}`
129
+
130
+ 结果: 替换为 "测试结果"
131
+
132
+ ### 2. 表格行循环
133
+
134
+ 在表格的某一行的单元格中写入 `{{#data.mutationSiteZero}}`,该行将成为模板行。
135
+
136
+ 其他单元格使用方括号占位符,如 `[gene_transcript]`、`[value]`。
137
+
138
+ 根据 `data.mutationSiteZero` 数组的长度,会自动生成对应数量的表格行。
139
+
140
+ **注意**: 表格行循环**不需要结束标签**,循环标记后直接跟字段占位符。
141
+
142
+ ### 3. 表格列循环
143
+
144
+ 在表格某一列的单元格中写入 `{{$data.mutationSiteZero}}`,该列将成为模板列。
145
+
146
+ 根据数据数组长度,会自动为所有行添加新列。
147
+
148
+ ### 4. 条件区块
149
+
150
+ 支持条件判断,**需要结束标签**:
151
+
152
+ ```text
153
+ {{?data.conclusion_C == ''}}无{{/}}
154
+ {{?data.conclusion_C != ''}}
155
+ {{*data.conclusion_C}}
156
+ {{/}}
157
+ ```
158
+
159
+ 支持的操作符:
160
+ - `!= null` / `== null`
161
+ - `== ''` / `!= ''` (检查空字符串)
162
+ - `size() > 0`
163
+ - `&&` (AND)
164
+ - `||` (OR)
165
+
166
+ 支持嵌套区块。
167
+
168
+ ### 5. 图片插入
169
+
170
+ 两种方式:
171
+
172
+ 方式1: 使用变量名
173
+ ```
174
+ {{@data.image_config}}
175
+ ```
176
+ 其中 `data.image_config` 是字典格式的配置。
177
+
178
+ 方式2: 直接写JSON
179
+ ```
180
+ {{@{'url':'https://example.com/image.jpg','conf':{'w':'180px','h':'180px'}}}}
181
+ ```
182
+
183
+ 支持的尺寸单位: px, cm, in, mm
184
+
185
+ 图片可以是HTTP/HTTPS链接或本地文件路径。
186
+
187
+ ### 6. 列表循环
188
+
189
+ 模板中写入: `{{*data.conclusion_C}}`
190
+
191
+ 数据中提供: `{'data': {'conclusion_C': ['项目1', '项目2', '项目3']}}`
192
+
193
+ 结果: 展开为多行文本,每行一个项目
194
+
195
+ ## 处理顺序
196
+
197
+ 引擎按以下顺序处理模板:
198
+
199
+ 1. **区块** - 先处理条件区块(因为它有结束标签,可能包含其他标签)
200
+ 2. **列表** - 处理列表循环
201
+ 3. **文本** - 处理文本占位符
202
+ 4. **图片** - 处理图片占位符
203
+ 5. **表格** - 最后处理表格(行循环和列循环)
204
+
205
+ ## 项目结构
206
+
207
+ ```
208
+ .
209
+ ├── pyproject.toml # 项目配置
210
+ ├── word_template_engine.py # 主引擎类(XML版本)
211
+ ├── example.py # 使用示例
212
+ ├── README.md # 详细文档
213
+ ├── template.docx # 模板文件
214
+ └── utils/ # 工具模块
215
+ ├── __init__.py
216
+ ├── xml_processor.py # XML处理工具
217
+ ├── text_processor.py # 文本处理
218
+ ├── table_processor.py # 表格处理(保留兼容)
219
+ ├── block_processor.py # 区块处理
220
+ ├── image_processor.py # 图片处理
221
+ └── list_processor.py # 列表处理
222
+ ```
223
+
224
+ ## 技术实现
225
+
226
+ 本引擎直接解析Word文档的XML结构(.docx文件实际上是一个ZIP压缩包,包含XML文件),这样可以:
227
+
228
+ - **完全保留样式信息**:所有格式、字体、颜色、对齐等样式都会保留
229
+ - **精确控制**:直接操作XML节点,可以精确处理复杂的文档结构
230
+ - **高性能**:无需通过中间库转换,直接操作XML
231
+
232
+ 主要处理流程:
233
+ 1. 解压.docx文件(ZIP格式)
234
+ 2. 解析 `word/document.xml` 主文档
235
+ 3. 在XML层面进行占位符替换和循环处理
236
+ 4. 重新打包为.docx文件
237
+
238
+ ## 注意事项
239
+
240
+ 1. 模板文件必须是有效的.docx格式
241
+ 2. 表格行循环标记需要放在表格的单元格中
242
+ 3. 表格行循环**不需要结束标签**,直接在标记后使用`[field]`占位符
243
+ 4. 只有**区块**需要结束标签 `{{/}}`
244
+ 5. 图片URL可以是HTTP/HTTPS链接或本地文件路径(图片功能在XML版本中暂未完整实现)
245
+ 6. 条件表达式支持基本的逻辑运算
246
+ 7. 嵌套的字段访问使用点号,如 `data.sub_field.value`
247
+ 8. 支持嵌套的条件区块
248
+ 9. **所有样式都会完整保留**,包括字体、颜色、大小、对齐等,这是使用XML解析的最大优势
pyproject.toml ADDED
@@ -0,0 +1,30 @@
1
+ [project]
2
+ name = "word-template-engine"
3
+ version = "0.1.0"
4
+ description = "A Word template engine similar to docx-template, implemented with python-docx and lxml."
5
+ readme = "README.md"
6
+ requires-python = ">=3.8"
7
+ authors = [
8
+ { name = "Your Name", email = "you@example.com" },
9
+ ]
10
+ license = { text = "MIT" }
11
+ dependencies = [
12
+ "requests>=2.28.0",
13
+ "python-docx>=1.1.0",
14
+ "lxml>=4.9.0",
15
+ ]
16
+
17
+ [project.urls]
18
+ Homepage = "https://github.com/yourname/word-template-engine"
19
+ Repository = "https://github.com/yourname/word-template-engine"
20
+
21
+ [build-system]
22
+ requires = ["hatchling"]
23
+ build-backend = "hatchling.build"
24
+
25
+ [tool.hatch.build.targets.sdist]
26
+ include = ["word_template_engine.py", "utils", "README.md", "pyproject.toml"]
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["."]
30
+
utils/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """
2
+ Utils package for word-template-engine.
3
+
4
+ This package contains the internal processors used by `WordTemplateEngine`:
5
+ - text_processor: text and block placeholders
6
+ - table_processor: table loops
7
+ - image_processor: image placeholders
8
+ """
9
+
10
+ __all__ = [
11
+ "text_processor",
12
+ "table_processor",
13
+ "image_processor",
14
+ "block_processor",
15
+ "list_processor",
16
+ ]
utils/common.py ADDED
@@ -0,0 +1,153 @@
1
+ """
2
+ Common utilities: nested value lookup and condition evaluation.
3
+ """
4
+ import re
5
+ import ast
6
+ import copy
7
+ from typing import Any, Dict
8
+
9
+
10
+ def get_nested_value(data: Dict[str, Any], path: str) -> Any:
11
+ """Get nested value from dict/list using dot path."""
12
+ if not path:
13
+ return None
14
+ copy_data = copy.deepcopy(data)
15
+ # dot_data = DotDict(data)
16
+ keys = path.split('.')
17
+ if not keys:
18
+ return copy_data[keys]
19
+ for i in range(len(keys)):
20
+ copy_data = copy_data.get(keys[i])
21
+
22
+ # value = dot_data.get_path(path)
23
+ # keys = path.split(".")
24
+ # value: Any = data
25
+ # for key in keys:
26
+ # if isinstance(value, dict):
27
+ # value = value.get(key)
28
+ # elif isinstance(value, list) and key.isdigit():
29
+ # idx = int(key)
30
+ # value = value[idx] if idx < len(value) else None
31
+ # else:
32
+ # return None
33
+ # if value is None:
34
+ # return None
35
+ return copy_data
36
+
37
+
38
+ def _value_len(val: Any) -> int:
39
+ if hasattr(val, "__len__"):
40
+ try:
41
+ return len(val) # type: ignore[arg-type]
42
+ except Exception:
43
+ return 0
44
+ return 0
45
+
46
+ def safe_eval(expr, variables=None):
47
+ if variables is None:
48
+ variables = {}
49
+
50
+ # 检查AST是否包含危险节点(如函数调用、属性访问)
51
+ tree = ast.parse(expr, mode='eval')
52
+ for node in ast.walk(tree):
53
+ if isinstance(node, (ast.Call, ast.Attribute)):
54
+ raise ValueError("Unsafe expression")
55
+
56
+ # 编译并执行
57
+ code = compile(tree, '<string>', 'eval')
58
+ return eval(code, {'__builtins__': {}}, variables)
59
+
60
+ def evaluate_condition(condition: str, data: Dict[str, Any]) -> bool:
61
+ """
62
+ Evaluate simple condition expressions used in templates.
63
+
64
+ Supported:
65
+ - == null / != null
66
+ - == '' / != ''
67
+ - size() > 0
68
+ - && / ||
69
+ - truthy check of a field
70
+ """
71
+ condition = condition.strip()
72
+ if not condition:
73
+ return False
74
+
75
+ # Logical AND / OR
76
+ if "&&" in condition:
77
+ return all(evaluate_condition(part, data) for part in condition.split("&&"))
78
+ if "||" in condition:
79
+ return any(evaluate_condition(part, data) for part in condition.split("||"))
80
+
81
+ # size() > 0
82
+ size_match = re.match(r"(.+)\.size(.*?)$", condition)
83
+ if size_match:
84
+ field = size_match.group(1).strip()
85
+ field2 = size_match.group(2).strip()
86
+ val = get_nested_value(data, field)
87
+ return safe_eval(f"{_value_len(val)}{field2}")
88
+
89
+ # == '' / != ''
90
+ empty_eq = re.match(r"(.+)==\s*['\"]\s*['\"]", condition)
91
+ if empty_eq:
92
+ field = empty_eq.group(1).strip()
93
+ val = get_nested_value(data, field)
94
+ return val == "" or val is None
95
+
96
+ empty_neq = re.match(r"(.+)!=\s*['\"]\s*['\"]", condition)
97
+ if empty_neq:
98
+ field = empty_neq.group(1).strip()
99
+ val = get_nested_value(data, field)
100
+ return val not in ("", None)
101
+
102
+ # == null / != null
103
+ null_eq = re.match(r"(.+)==\s*null", condition, re.IGNORECASE)
104
+ if null_eq:
105
+ field = null_eq.group(1).strip()
106
+ val = get_nested_value(data, field)
107
+ return val is None
108
+
109
+ null_neq = re.match(r"(.+)!=( )?null", condition, re.IGNORECASE)
110
+ if null_neq:
111
+ field = null_neq.group(1).strip()
112
+ val = get_nested_value(data, field)
113
+ return val is not None
114
+
115
+ # Direct truthy check
116
+ val = get_nested_value(data, condition)
117
+ return bool(val)
118
+
119
+
120
+ def process_block(text: str, data: Dict[str, Any]) -> str:
121
+ """
122
+ Process conditional blocks {{?condition}}...{{/}}.
123
+ Removes blocks when condition is False; strips tags when True.
124
+ """
125
+ start_pat = r"\{\{\?([^}]+)\}\}"
126
+ end_pat = r"\{\{/\}\}"
127
+
128
+ def find_blocks(s: str):
129
+ starts = list(re.finditer(start_pat, s))
130
+ ends = list(re.finditer(end_pat, s))
131
+ return starts, ends
132
+
133
+ result = text
134
+ while True:
135
+ starts, ends = find_blocks(result)
136
+ if not starts or not ends:
137
+ break
138
+ # pair first start with nearest end after it (no deep nesting handling)
139
+ start = starts[0]
140
+ end_candidates = [e for e in ends if e.start() > start.end()]
141
+ if not end_candidates:
142
+ break
143
+ end = end_candidates[0]
144
+ inner = result[start.end(): end.start()]
145
+ cond = start.group(1).strip()
146
+ if evaluate_condition(cond, data):
147
+ # keep inner, drop tags
148
+ replacement = inner
149
+ else:
150
+ replacement = ""
151
+ result = result[: start.start()] + replacement + result[end.end():]
152
+ return result
153
+