短代码 [Archives] 大写首字母不生效(WP 标签区分大小写,我 3.2.46 的注释写反了) #17

Closed
opened 2026-10-26 05:55:03 +00:00 by laoyang · 3 comments
Owner

现象

在页面 /blogtime(ID 522)正文里写 [Archives],前台原样输出成 <p>[Archives]</p>,什么都没渲染。

服务器主题已是 3.2.46,inc/theme-archives.php 在、functions.php 第 66 行也 require 了,
add_shortcode('archives', ...) 也注册成功(WP 环境下 $shortcode_tags['archives'] 可调用)。

根因

WordPress 的短代码标签是区分大小写的,我 3.2.46 里的注释写反了。

我在 inc/theme-archives.php 末尾写的是:

WP 的 shortcode 标签是大小写不敏感的(它内部统一 strtolower),
所以注册小写 archives 就能同时匹配 [Archives] / [ARCHIVES] / [archives]。

这句话是错的,内核证据:

  1. wp-includes/shortcodes.php:330

    $tagregexp = implode( '|', array_map( 'preg_quote', $tagnames ) );
    

    直接拿注册名拼正则,没有做任何大小写归一化。

  2. wp-includes/shortcodes.php:155 / 189

    preg_match_all( '/' . get_shortcode_regex() . '/', $content, $matches, PREG_SET_ORDER );
    

    修饰符只有默认,没有 i ⇒ 正则大小写敏感。

  3. do_shortcode_tag() 里 $tag = $m[2] 之后直接
    $shortcode_tags[$tag] 精确键查表 ⇒ 名字对不上就是查不到。

所以 add_shortcode('archives', ...) 只能匹配全小写 [archives],
文档里写的 [Archives](首字母大写)压根匹配不上,WordPress 就把原文原样吐出来。

实测证据(线上 WP 环境直接跑 do_shortcode)

[archives]       长度=209239 是否被解析=是
[Archives]       长度=10     是否被解析=否(原样返回)
[ARCHIVES]       长度=10     是否被解析=否(原样返回)
[aRcHiVeS]       长度=10     是否被解析=否(原样返回)

[archives] 小写是正常的:渲染出 15 篇文章、12 个月份分组、年份标题 2026。

责任归属

  • ❌ 不是 markdown 编辑器插件的锅。数据库里存的原始字节是
    5b41726368697665735d = [Archives],没有被转义(没有 &#91;、没有 \[)。
  • ❌ 不是缓存(绕缓存加随机查询串重抓,输出不变)。
  • ✅ 是我 3.2.46 的实现缺陷:注册名与文档承诺的写法不一致,且注释给了错误结论。

修复方案

同时注册两种大小写,覆盖用户最可能写的形态,并修正注释:

add_shortcode('archives', 'yang_archives_shortcode');
add_shortcode('Archives', 'yang_archives_shortcode');

⚠️ 注意不能靠 strtolower 之类"全局改",因为 WP 的 regex 是从 $shortcode_tags
的 key 直接拼出来的,唯一的办法就是把要支持的写法逐个注册。

同时 shortcode_atts() 第 3 个参数('archives')也要跟着改 —— 它只用于过滤
shortcode_atts_<tag> 钩子,两种注册下都要能命中,所以传当前实际匹配到的 tag。

验收标准

  1. [Archives](首字母大写)在页面上正常渲染归档列表。
  2. [archives](全小写)仍然正常(不能修一个坏一个)。
  3. 其余参数(year= / month= / nav=)在两种写法下都生效。
  4. 补回归测试:断言两种大小写都能渲染,并加反向验证(改回只注册一种必须变红)。
  5. 发布 3.2.47,更新 readme。
## 现象 在页面 `/blogtime`(ID 522)正文里写 `[Archives]`,前台**原样输出**成 `<p>[Archives]</p>`,什么都没渲染。 服务器主题已是 3.2.46,`inc/theme-archives.php` 在、`functions.php` 第 66 行也 require 了, `add_shortcode('archives', ...)` 也注册成功(WP 环境下 `$shortcode_tags['archives']` 可调用)。 ## 根因 **WordPress 的短代码标签是区分大小写的,我 3.2.46 里的注释写反了。** 我在 `inc/theme-archives.php` 末尾写的是: > WP 的 shortcode 标签是**大小写不敏感**的(它内部统一 strtolower), > 所以注册小写 `archives` 就能同时匹配 [Archives] / [ARCHIVES] / [archives]。 这句话**是错的**,内核证据: 1. `wp-includes/shortcodes.php:330` ```php $tagregexp = implode( '|', array_map( 'preg_quote', $tagnames ) ); ``` 直接拿注册名拼正则,**没有做任何大小写归一化**。 2. `wp-includes/shortcodes.php:155 / 189` ```php preg_match_all( '/' . get_shortcode_regex() . '/', $content, $matches, PREG_SET_ORDER ); ``` 修饰符只有默认,**没有 `i`** ⇒ 正则大小写敏感。 3. `do_shortcode_tag()` 里 `$tag = $m[2]` 之后直接 `$shortcode_tags[$tag]` 精确键查表 ⇒ 名字对不上就是查不到。 所以 `add_shortcode('archives', ...)` **只能匹配全小写 `[archives]`**, 文档里写的 `[Archives]`(首字母大写)压根匹配不上,WordPress 就把原文原样吐出来。 ### 实测证据(线上 WP 环境直接跑 do_shortcode) ``` [archives] 长度=209239 是否被解析=是 [Archives] 长度=10 是否被解析=否(原样返回) [ARCHIVES] 长度=10 是否被解析=否(原样返回) [aRcHiVeS] 长度=10 是否被解析=否(原样返回) ``` `[archives]` 小写是**正常的**:渲染出 15 篇文章、12 个月份分组、年份标题 2026。 ## 责任归属 - ❌ **不是** markdown 编辑器插件的锅。数据库里存的原始字节是 `5b41726368697665735d` = `[Archives]`,**没有被转义**(没有 `&#91;`、没有 `\[`)。 - ❌ 不是缓存(绕缓存加随机查询串重抓,输出不变)。 - ✅ **是我 3.2.46 的实现缺陷**:注册名与文档承诺的写法不一致,且注释给了错误结论。 ## 修复方案 同时注册两种大小写,覆盖用户最可能写的形态,并修正注释: ```php add_shortcode('archives', 'yang_archives_shortcode'); add_shortcode('Archives', 'yang_archives_shortcode'); ``` ⚠️ 注意不能靠 `strtolower` 之类"全局改",因为 WP 的 regex 是从 `$shortcode_tags` 的 key 直接拼出来的,**唯一的办法就是把要支持的写法逐个注册**。 同时 `shortcode_atts()` 第 3 个参数(`'archives'`)也要跟着改 —— 它只用于过滤 `shortcode_atts_<tag>` 钩子,两种注册下都要能命中,所以传当前实际匹配到的 tag。 ## 验收标准 1. `[Archives]`(首字母大写)在页面上正常渲染归档列表。 2. `[archives]`(全小写)仍然正常(不能修一个坏一个)。 3. 其余参数(`year=` / `month=` / `nav=`)在两种写法下都生效。 4. 补回归测试:**断言两种大小写都能渲染**,并加反向验证(改回只注册一种必须变红)。 5. 发布 3.2.47,更新 readme。
Author
Owner

已修好并发布 3.2.47

先回答你的猜测:不是 markdown 插件的问题,它没有转义任何东西。

数据库里存的原始字节是 5b41726368697665735d = [Archives],
干干净净,没有 &#91;、没有 \[。页面输出 <p>[Archives]</p> 是 WordPress
把没匹配上的短代码原样吐出的默认行为。


根因:我 3.2.46 只注册了小写标签

WordPress 的短代码标签区分大小写。我在 3.2.46 里写的注释说"大小写不敏感、内部统一 strtolower"——这句是错的。

内核证据(wp-includes/shortcodes.php):

位置 代码 说明
L330 `$tagregexp = implode(' ', array_map('preg_quote', $tagnames));`
L155 preg_match_all('/'.get_shortcode_regex().'/', ...) 修饰符只有默认,没有 i
L54 if (!is_callable($shortcode_tags[$tag])) $m[2] 精确查表

所以 add_shortcode('archives', ...) 只能匹配全小写。你按文档写 [Archives],正则压根不认。

线上实测(真实 WP 环境跑 do_shortcode,不是模拟):

[archives]       长度=209239  解析=是
[Archives]       长度=10      解析=否(原样返回)
[ARCHIVES]       长度=10      解析=否(原样返回)
[aRcHiVeS]       长度=10      解析=否(原样返回)

顺带抓出第二个 bug:短代码签名漏了中间那个参数

改签名时发现原来的 yang_archives_shortcode($atts, $tag = 'archives') 也错了。

WordPress 按 call_user_func($fn, $attr, $content, $tag) 传三个参数,
我漏了中间的 $content,于是 $tag 落到了第 2 个形参上、实际收到空串。

后果比较隐蔽:归档看起来能用(默认值恰好对),但 shortcode_atts_Archives
这个开发者扩展钩子退化成无主的 shortcode_atts_,等于悄悄失效。

这个坑有意思在于:直接调函数是好的(f($atts, 'Archives') 完全正确),
只有过 do_shortcode 才错。所以断言必须走真实解析路径才抓得到。


改了什么

inc/theme-archives.php

  • 同时注册 archives / Archives / ARCHIVES 三种写法
  • 签名改成 ($atts, $content = '', $tag = 'archives')
  • shortcode_atts 第 3 参数改为实际命中的 $tag(原来写死 'archives')

style.css 版本号 3.2.46 → 3.2.47

[aRcHiVeS](任意混合大小写)刻意不支持 —— 那要把每个位置都注册一条,
正则会膨胀到不可维护。三种习惯写法已覆盖真实使用。已加断言锁住这个取舍,
避免将来有人当 bug 报。


测试也一起修了(这是本轮最该说的)

原来的测试是假绿的,而且绿得很彻底。

_archives_unit.php 里 add_shortcode 的测试替身写的是:

$GLOBALS['__shortcodes'][strtolower($tag)] = $fn;   // ← 多做了 WP 不做的事

它比真实 WordPress 更宽容。于是产品在线上坏着,本地 94 条断言全绿。
断言本身也软:只查 isset($__shortcodes['archives']),
从没验过 [Archives] 这个用户实际会写的形态。

修法:

  1. 桩照抄内核 —— 不做 strtolower
  2. 新增真实的 do_shortcode() 解析实现(大小写敏感、与内核同构的正则)
  3. ⑪ 组从"查注册表有没有这个键"改成拿真实写法解析一遍
  4. 补断言:三种写法都渲染出 .yang-archives、混合大小写原样返回、
    未注册标签原样保留、shortcode_atts 收到的 tag 恰好是命中的那个

反向验证 4 条(把错误改回去必须变红):

R5 只注册全小写 archives(3.2.46 的错误形态)  → 红 10 条
R6 大写注册指向不存在的回调                   → 红  4 条
R7 shortcode_atts 的 tag 写死小写              → 红  2 条
R8 签名去掉中间的 $content                     → 红  3 条

验证结果

2j 评论关闭零输出      通过  54 / 0 失败
2k 评论关闭反向验证    真红  13 / 0 无效
2l 归档单测            通过 109 / 0 失败   (原 94,新增 15 条)
2m 归档端到端          通过 109 / 0 失败
2n 归档端到端反向      真红  21 / 0 无效
2o 归档源码级反向      真红  10 / 0 无效   (原 6,新增 4 条)
lint  96 个 PHP 文件全过

线上真实 WordPress 验证(在服务器上用 WP 内核跑,不是本地模拟):

补注册前:只 ["archives"]           → [Archives] 原样返回
补注册后:["archives","Archives","ARCHIVES"]
          [archives] 长度=209239  解析=是
          [Archives] 长度=209239  解析=是
          [ARCHIVES] 长度=209239  解析=是
          [aRcHiVeS] 长度=10      解析=否(刻意)

发布状态

  • 已推 3.2.47 到 /theme-update/,update.json 已落 3.2.47
  • 下载包 4,739,538 字节 / 235 文件,包内 Version: 3.2.47,4 处 add_shortcode
  • readme 版本标题 37 → 38,未截断
  • 提交 db2e9c4 已推 main
  • 服务器主题目录仍是 3.2.46,未被覆盖

请你验一下

  1. WP 后台「外观 → 主题」应该能看到 3.2.47 可更新,点升级。
  2. 回到 /blogtime 刷新,不需要改页面内容 —— [Archives] 会自动开始渲染。
  3. 顺手确认 [archives](全小写)也还正常,别修一个坏一个。

如果后台没出现更新提示,可能是更新检查有缓存,等一会儿或去「仪表盘 → 更新」手动点一次"重新检查"。

工单先不关,你确认没问题再关。

## 已修好并发布 3.2.47 **先回答你的猜测:不是 markdown 插件的问题,它没有转义任何东西。** 数据库里存的原始字节是 `5b41726368697665735d` = `[Archives]`, 干干净净,没有 `&#91;`、没有 `\[`。页面输出 `<p>[Archives]</p>` 是 WordPress **把没匹配上的短代码原样吐出**的默认行为。 --- ## 根因:我 3.2.46 只注册了小写标签 WordPress 的短代码标签**区分大小写**。我在 3.2.46 里写的注释说"大小写不敏感、内部统一 strtolower"——**这句是错的**。 内核证据(`wp-includes/shortcodes.php`): | 位置 | 代码 | 说明 | |---|---|---| | L330 | `$tagregexp = implode('|', array_map('preg_quote', $tagnames));` | 拿注册名直接拼正则,**不做归一化** | | L155 | `preg_match_all('/'.get_shortcode_regex().'/', ...)` | 修饰符只有默认,**没有 `i`** | | L54 | `if (!is_callable($shortcode_tags[$tag]))` | `$m[2]` 精确查表 | 所以 `add_shortcode('archives', ...)` **只能匹配全小写**。你按文档写 `[Archives]`,正则压根不认。 **线上实测(真实 WP 环境跑 `do_shortcode`,不是模拟):** ``` [archives] 长度=209239 解析=是 [Archives] 长度=10 解析=否(原样返回) [ARCHIVES] 长度=10 解析=否(原样返回) [aRcHiVeS] 长度=10 解析=否(原样返回) ``` --- ## 顺带抓出第二个 bug:短代码签名漏了中间那个参数 改签名时发现原来的 `yang_archives_shortcode($atts, $tag = 'archives')` 也错了。 WordPress 按 `call_user_func($fn, $attr, $content, $tag)` **传三个参数**, 我漏了中间的 `$content`,于是 `$tag` 落到了第 2 个形参上、实际收到空串。 后果比较隐蔽:归档**看起来能用**(默认值恰好对),但 `shortcode_atts_Archives` 这个开发者扩展钩子退化成无主的 `shortcode_atts_`,等于悄悄失效。 这个坑有意思在于:**直接调函数是好的**(`f($atts, 'Archives')` 完全正确), **只有过 `do_shortcode` 才错**。所以断言必须走真实解析路径才抓得到。 --- ## 改了什么 **`inc/theme-archives.php`** - 同时注册 `archives` / `Archives` / `ARCHIVES` 三种写法 - 签名改成 `($atts, $content = '', $tag = 'archives')` - `shortcode_atts` 第 3 参数改为**实际命中的 `$tag`**(原来写死 `'archives'`) **`style.css`** 版本号 3.2.46 → 3.2.47 > `[aRcHiVeS]`(任意混合大小写)**刻意不支持** —— 那要把每个位置都注册一条, > 正则会膨胀到不可维护。三种习惯写法已覆盖真实使用。已加断言锁住这个取舍, > 避免将来有人当 bug 报。 --- ## 测试也一起修了(这是本轮最该说的) **原来的测试是假绿的,而且绿得很彻底。** `_archives_unit.php` 里 `add_shortcode` 的测试替身写的是: ```php $GLOBALS['__shortcodes'][strtolower($tag)] = $fn; // ← 多做了 WP 不做的事 ``` 它比真实 WordPress **更宽容**。于是产品在线上坏着,本地 94 条断言全绿。 断言本身也软:只查 `isset($__shortcodes['archives'])`, **从没验过 `[Archives]` 这个用户实际会写的形态**。 修法: 1. 桩照抄内核 —— 不做 `strtolower` 2. 新增真实的 `do_shortcode()` 解析实现(大小写敏感、与内核同构的正则) 3. ⑪ 组从"查注册表有没有这个键"改成**拿真实写法解析一遍** 4. 补断言:三种写法都渲染出 `.yang-archives`、混合大小写原样返回、 未注册标签原样保留、`shortcode_atts` 收到的 tag 恰好是命中的那个 **反向验证 4 条(把错误改回去必须变红):** ``` R5 只注册全小写 archives(3.2.46 的错误形态) → 红 10 条 R6 大写注册指向不存在的回调 → 红 4 条 R7 shortcode_atts 的 tag 写死小写 → 红 2 条 R8 签名去掉中间的 $content → 红 3 条 ``` --- ## 验证结果 ``` 2j 评论关闭零输出 通过 54 / 0 失败 2k 评论关闭反向验证 真红 13 / 0 无效 2l 归档单测 通过 109 / 0 失败 (原 94,新增 15 条) 2m 归档端到端 通过 109 / 0 失败 2n 归档端到端反向 真红 21 / 0 无效 2o 归档源码级反向 真红 10 / 0 无效 (原 6,新增 4 条) lint 96 个 PHP 文件全过 ``` **线上真实 WordPress 验证**(在服务器上用 WP 内核跑,不是本地模拟): ``` 补注册前:只 ["archives"] → [Archives] 原样返回 补注册后:["archives","Archives","ARCHIVES"] [archives] 长度=209239 解析=是 [Archives] 长度=209239 解析=是 [ARCHIVES] 长度=209239 解析=是 [aRcHiVeS] 长度=10 解析=否(刻意) ``` --- ## 发布状态 - 已推 **3.2.47** 到 `/theme-update/`,`update.json` 已落 `3.2.47` - 下载包 4,739,538 字节 / 235 文件,包内 `Version: 3.2.47`,4 处 `add_shortcode` - readme 版本标题 37 → 38,未截断 - 提交 `db2e9c4` 已推 `main` - **服务器主题目录仍是 3.2.46,未被覆盖** ## 请你验一下 1. WP 后台「外观 → 主题」应该能看到 **3.2.47 可更新**,点升级。 2. 回到 `/blogtime` 刷新,**不需要改页面内容** —— `[Archives]` 会自动开始渲染。 3. 顺手确认 `[archives]`(全小写)也还正常,别修一个坏一个。 > 如果后台没出现更新提示,可能是更新检查有缓存,等一会儿或去「仪表盘 → 更新」手动点一次"重新检查"。 工单先不关,你确认没问题再关。
Author
Owner

线上已生效 —— 实测通过

你已经点过升级了,服务器主题目录现在是 3.2.47(注册条数 4)。
刚抓了线上真实页面确认:

GET https://yangyq.net/blogtime   200 / 252,647 字节
检查项 结果
页面上还残留 [Archives] 原样文字 0 处(已消失)
.yang-archives 根容器 1 个
年份标题 2026
条目数 ar-item 15 篇
月份分组 ar-month 4 个(1 月 7 篇 …)
统计数字 ar-stat 45 个(15 篇 × 浏览/评论/点赞)
四角导航 ar-nav 2 处(上排 + 下排各一对)
上一年按钮 is-disabled(2026 是今年,正确禁用)
下一年按钮 指向 ?arc_year=2025(正确)

渲染出来的结构长这样:

<div class="yang-archives" id="yang-archives-1" data-year="2026">
  <div class="ar-head">
    <h3 class="ar-year" data-role="year-title">2026</h3>
    <span class="ar-total" data-role="year-total">共 15 篇</span>
    <div class="ar-nav ar-nav-top">...</div>
  </div>
  <div class="ar-body" data-role="body">
    <div class="ar-month">
      <h4 class="ar-month-title">1 月<span class="ar-month-count">7</span></h4>
      <ul class="ar-list"><li class="ar-item">...</li></ul>
    </div>
    ...
  </div>
</div>

别忘了验这几下(JS 部分静态抓页看不出来)

抓页只能证明服务端首屏对了。下面三条要靠你点一下:

  1. 点右下角「下一年」→ 年份变 2025,页面不刷新(滚动位置不跳、不白屏),
    地址栏变成 ?arc_year=2025。
  2. 刷新一下 → 还是 2025(说明地址同步生效,链接可以直接分享)。
  3. 点「上一年」回到 2026,此时「上一年」按钮应该是灰的。

状态

  • 3.2.47 已发布,线上已升级生效
  • 提交 db2e9c4(带 #17)
  • 六套件全绿:54/0 · 13 真红 · 109/0 · 109/0 · 21 真红 · 10 真红
  • 96 个 PHP 文件 lint 全过

你点完上面三条没问题的话,我就关掉这个工单。(也可以直接回一句"好了")

## 线上已生效 —— 实测通过 你已经点过升级了,服务器主题目录现在是 **3.2.47**(注册条数 4)。 刚抓了线上真实页面确认: ``` GET https://yangyq.net/blogtime 200 / 252,647 字节 ``` | 检查项 | 结果 | |---|---| | 页面上还残留 `[Archives]` 原样文字 | **0 处**(已消失) | | `.yang-archives` 根容器 | 1 个 | | 年份标题 | **2026** | | 条目数 `ar-item` | **15 篇** | | 月份分组 `ar-month` | **4 个**(1 月 7 篇 …) | | 统计数字 `ar-stat` | **45 个**(15 篇 × 浏览/评论/点赞) | | 四角导航 `ar-nav` | **2 处**(上排 + 下排各一对) | | 上一年按钮 | `is-disabled`(2026 是今年,正确禁用) | | 下一年按钮 | 指向 `?arc_year=2025`(正确) | 渲染出来的结构长这样: ```html <div class="yang-archives" id="yang-archives-1" data-year="2026"> <div class="ar-head"> <h3 class="ar-year" data-role="year-title">2026</h3> <span class="ar-total" data-role="year-total">共 15 篇</span> <div class="ar-nav ar-nav-top">...</div> </div> <div class="ar-body" data-role="body"> <div class="ar-month"> <h4 class="ar-month-title">1 月<span class="ar-month-count">7</span></h4> <ul class="ar-list"><li class="ar-item">...</li></ul> </div> ... </div> </div> ``` --- ## 别忘了验这几下(JS 部分静态抓页看不出来) 抓页只能证明**服务端首屏**对了。下面三条要靠你点一下: 1. 点右下角「下一年」→ 年份变 **2025**,**页面不刷新**(滚动位置不跳、不白屏), 地址栏变成 `?arc_year=2025`。 2. 刷新一下 → 还是 2025(说明地址同步生效,链接可以直接分享)。 3. 点「上一年」回到 2026,此时「上一年」按钮应该是灰的。 --- ## 状态 - 3.2.47 已发布,线上已升级生效 - 提交 `db2e9c4`(带 #17) - 六套件全绿:54/0 · 13 真红 · 109/0 · 109/0 · 21 真红 · 10 真红 - 96 个 PHP 文件 lint 全过 **你点完上面三条没问题的话,我就关掉这个工单。**(也可以直接回一句"好了")
Author
Owner

按用户确认收尾,交付内容见上条发布评论。

交付版本:3.2.47(修 [Archives] 大写首字母不生效,WP 短代码标签区分大小写)

用户已验收(线上主题已升级至 3.2.58,包含本单全部改动),关闭本单。

按用户确认收尾,交付内容见上条发布评论。 交付版本:3.2.47(修 [Archives] 大写首字母不生效,WP 短代码标签区分大小写) 用户已验收(线上主题已升级至 3.2.58,包含本单全部改动),关闭本单。
Sign in to join this conversation.
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: laoyang/wordpress-kratos-yang#17