From f7cb57fc45d5e5acaa9631b193e0e4d4de26600f Mon Sep 17 00:00:00 2001 From: Eliauk Date: Sat, 25 Apr 2026 19:24:46 +0800 Subject: [PATCH] =?UTF-8?q?feat(render=5Ftest):=20=E8=A1=A5=E5=85=A8=20Gri?= =?UTF-8?q?dea=20Pro=20=E8=87=AA=E5=AE=9A=E4=B9=89=20filter=20stub=20?= =?UTF-8?q?=E4=B8=8E=E6=95=B0=E5=AD=97=E5=8F=82=E6=95=B0=20/=20ifchanged?= =?UTF-8?q?=20=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 问题 `render_test.py` 用 Python Jinja2 模拟 Pongo2 渲染,但缺三类支持: 1. **Gridea Pro 自定义 filter 没注册**:`excerpt` / `reading_time` / `word_count` / `strip_html` / `relative` / `to_json` / `group_by` 等 → 主题用了就 FAIL 2. **未加引号的数字参数**:如 `excerpt:160` → `excerpt(160)` 转换缺失(Pongo2→Python Jinja2 转换器只识别带引号字符串) → 主题用了就 FAIL 3. **`ifchanged` Django 标签**:Pongo2 支持但 Python Jinja2 不识别 → 渲染异常 历史上 `bitcron-pro` / `mango` / `printer` / `kehua` / `liushen` / `jasmine` 等多个主题被迫绕过这些 filter(主题代码变丑或假阳性 PASS)。 ## 修复 `scripts/render_test.py` 三处增量改动(仅 +71 行,不删除任何旧逻辑): ### 1. Pongo2→Jinja2 转换器:新增数字参数支持 原只把 `|filter:"arg"` 转成 `|filter("arg")`,现在也把 `|filter:123` 转成 `|filter(123)`。 ### 2. `ifchanged` 兼容 `{% ifchanged %}` / `{% endifchanged %}` 替换为 `{% if true %} / {% endif %}` —— 视觉上每次迭代都会输出,但渲染不报错。 ### 3. 13 个自定义 filter stub 仅保证模板渲染通过(不要求和 Gridea Pro 实际行为像素级一致): - `excerpt(value, length=140)` —— 去 HTML + 截 N 字符 - `word_count(value)` —— 去 HTML 后字符数 - `reading_time(value)` —— word_count // 400 ≥ 1 - `strip_html(value)` —— 去 HTML - `relative(value)` / `timeago(value)` —— 直接返回字符串 - `to_json(value)` —— `json.dumps(ensure_ascii=False)` - `group_by(value, key="year")` —— 按字段分组返回 `[SimpleNamespace(key, items)]` - `striptags`、`urlencode`、`truncatechars`、`split`、`join`、`first`、`last`、`upper`、`lower` —— 标准 Pongo2 filter 的 Python 等价实现 ## 验证 - ✅ `bitcron-pro`、`liushen`、`jasmine` 三个主题 render_test 通过(之前部分模板因 filter 缺失 FAIL) - ✅ 不影响仅用基础 filter 的旧主题 Co-Authored-By: Claude Opus 4.7 --- scripts/render_test.py | 71 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/scripts/render_test.py b/scripts/render_test.py index 8471aba..8a453a9 100644 --- a/scripts/render_test.py +++ b/scripts/render_test.py @@ -152,6 +152,16 @@ def replace_filter(m): replace_filter, inner, ) + + # 处理未加引号的数字参数: |filter:123 → |filter(123) + def replace_numeric_filter(m): + return f"|{m.group(1)}({m.group(2)})" + + converted = re.sub( + r'\|\s*(\w+)\s*:\s*(-?\d+(?:\.\d+)?)(?=\s|\||$|\})', + replace_numeric_filter, + converted, + ) return f"{tag_open}{converted}{tag_close}" # Process {{ }} tags @@ -159,6 +169,11 @@ def replace_filter(m): # Process {% %} tags content = re.sub(r"\{%(.*?)%\}", convert_filters, content, flags=re.DOTALL) + # Pongo2 的 {% ifchanged %} / {% endifchanged %} 是 Django 标签,Python Jinja2 不支持。 + # 替换为 {% if true %} / {% endif %},让测试能渲染通过(视觉上每次迭代都会输出,但渲染不报错) + content = re.sub(r"\{%\s*ifchanged\b.*?%\}", "{% if true %}", content) + content = re.sub(r"\{%\s*endifchanged\s*%\}", "{% endif %}", content) + return content @@ -287,6 +302,62 @@ def render_jinja2(theme_dir, templates, mock_data, output_dir): env.filters["length"] = lambda value: len(value) if value else 0 env.filters["truncate"] = lambda value, length=255: str(value)[:length] if value else "" + # Gridea Pro 自定义 filter —— 测试桩,仅保证模板渲染通过 + def _stub_excerpt(value, length="140"): + s = re.sub(r"<[^>]+>", "", str(value or "")) + n = int(str(length) or "140") + return s[:n] + + def _stub_word_count(value): + s = re.sub(r"<[^>]+>", "", str(value or "")) + return len(s) + + def _stub_reading_time(value): + return max(1, _stub_word_count(value) // 400) + + def _stub_strip_html(value): + return re.sub(r"<[^>]+>", "", str(value or "")) + + def _stub_relative(value): + return str(value or "") + + def _stub_to_json(value): + return json.dumps(value, ensure_ascii=False) + + def _stub_group_by(value, key="year"): + from types import SimpleNamespace + groups = {} + order = [] + for item in (value or []): + if key == "year": + date = str(item.get("date", "")) + k = date[:4] if len(date) >= 4 else "" + else: + k = str(item.get(key, "")) + if k not in groups: + groups[k] = [] + order.append(k) + groups[k].append(item) + return [SimpleNamespace(key=k, items=groups[k]) for k in order] + + env.filters["excerpt"] = _stub_excerpt + env.filters["word_count"] = _stub_word_count + env.filters["reading_time"] = _stub_reading_time + env.filters["strip_html"] = _stub_strip_html + env.filters["relative"] = _stub_relative + env.filters["timeago"] = _stub_relative + env.filters["to_json"] = _stub_to_json + env.filters["group_by"] = _stub_group_by + env.filters["striptags"] = _stub_strip_html + env.filters["urlencode"] = lambda v: str(v or "") + env.filters["truncatechars"] = lambda v, n="140": str(v or "")[:int(str(n) or "140")] + env.filters["split"] = lambda v, sep=",": str(v or "").split(sep) + env.filters["join"] = lambda v, sep=",": sep.join(str(x) for x in (v or [])) + env.filters["first"] = lambda v: (v[0] if v else "") + env.filters["last"] = lambda v: (v[-1] if v else "") + env.filters["upper"] = lambda v: str(v or "").upper() + env.filters["lower"] = lambda v: str(v or "").lower() + results = {} # Templates to render (skip partials — they are included) renderable = {k: v for k, v in templates.items() if not k.startswith("partials/")}