Files
wordpress-kratos-yang/comments.php
T
laoyang 7047ab3ece 3.2.46: 新增 [Archives] 归档短代码 + 评论关闭时整块零输出 #15
需求一:[Archives] 短代码 —— 归档式全站文章列表
  · 在文章/页面正文写一行 [Archives],该位置渲染全站文章的年月归档
  · 默认显示今年,按月份归类;每篇带浏览/评论/点赞三个数
  · 上下两排各有一组「上一年 / 下一年」按钮(四角),JS 切换不刷新页面
  · 地址栏同步 ?arc_year=,刷新与分享链接都能保持同一年
  · 只在用到短代码的页面才加载 assets/js|css/archives.js|css
  · 参数:year="2025" 指定首年;month="0" 不按月分组

需求二:评论关闭时评论区整块消失
  · comments.php 在**任何输出之前** `if (!comments_open()) return;`
    —— 零字节,不是空容器、不是 display:none、不留提示文字
  · 已有评论的老文章关评论后也不再显示(需求原话:正文之外什么都没有)
  · inc/theme-comments.php 的资源守卫与之同口径:关评论时不加载
    约 10KB 的 comments.css / comments.js
  · 删掉 .comments-closed 样式与两处「评论已关闭」提示分支

开发中实测抓到并修掉的三个真实 bug(都是"测试把期望值写对之后"才显形):
  · 年份标题 / 导航年份过了 number_format_i18n() ⇒ 输出「2,025」,
    而 JS 侧 String(year) 输出「2025」⇒ 首屏与切年后不一致
  · 月份标题同样过 number_format_i18n(),与 JS 侧 String(m) 口径不一致
  · yang_archives_render_nav() 把 ar-nav-top 写死,而下排也调它生成
    ⇒ 下排被叫成 top(视觉上被更高特异性规则压住,看不出问题)

验证:
  归档  _archives_unit.php        94 通过 / 0 失败
        _archives_e2e.js        109 通过 / 0 失败
        _archives_e2e_falsify.js 反证 25 真红 / 0 无效
        _archives_unit_falsify.py 反证 6 真红 / 0 无效
  评论  _comments_closed_test.php 54 通过 / 0 失败
        _comments_closed_falsify.py 反证 13 真红 / 0 无效
  回归  _test_comments.php        64 通过 / 0 失败
        _hl_unit_run.py          24 通过 / 0 失败
  全量  96 个 PHP 文件语法检查通过;包内 lint 通过
  发布  已推 3.2.46 到 /theme-update/,update.json 已落 3.2.46;
        服务器 wp-content/themes/kratos_yang/ 仍为 3.2.45(未覆盖)

另:把 _archives_unit.php 从 `__DIR__` 定位改为接受根目录参数,
    否则它无法登记进 verify_all.sh,只能手动跑(手动跑 = 长期不跑)。
    5 个新套件已登记进 verify_all.sh(2j ~ 2o)。
2026-10-26 13:25:51 +08:00

460 lines
23 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 评论模板
*
* ── 3.2.18 重写要点 ──────────────────────────────────────────
* 1. **列表容器改成 <ol>**。原来 `<div class="list">` 里直接放
* `wp_list_comments()` 输出的 `<li>`,标签层级非法。浏览器会
* 自行"修复"结构,而嵌套回复(`<ul class="children">`)在这种
* 被修复过的树里容易错位 —— 这是"评论看起来乱"的一个真实来源。
*
* 2. **回复状态条**。点「回复」时除了把表单挪过去,还在表单上方
* 显示「正在回复 @某人 / 取消」。旧实现只有表单被挪动这一条线索,
* 用户在长评论区里挪动后常常不知道自己到底在回复谁。
*
* 3. **表单加 nonce**。AJAX 提交走 nonce 校验,没有它任何第三方
* 页面都能借访客的浏览器刷评论。
*
* 4. **提交按钮的禁用态由 JS 控制**,模板里给出 `.is-loading` 的样式钩子。
*
* ── 3.2.19 修的两个结构问题 ──────────────────────────────────
* 5. **取数不再交给 `wp_list_comments()` 推导**。3.2.18 传了
* `page=1` / `per_page=10`,而核心那段"独立查询"分支在内层
* 条件不成立时**不会给 `$_comments` 赋值**(注释说会回退到
* `$wp_query->comments`,但代码里没有这个回退),结果
* `paged_walk(null, ...)` 输出空串 —— 评论一条都不显示。
* 现在由 `yang_comment_page()` 取好、当第二个参数传进去。
*
* 6. **子评论容器改成 `yang-children`**(见 `Yang_Walker_Comment`)。
* 核心 `start_lvl()` 固定输出 `class="children"`,而样式表里
* 几十条规则全按 `.yang-children` 写,类名对不上就全部失效。
*
* @author Yongquan Yang <yangyq.net>
* @license MIT License
*/
if (isset($_SERVER['SCRIPT_FILENAME']) && 'comments.php' == basename($_SERVER['SCRIPT_FILENAME'])) {
die();
}
/* ── 3.2.46 评论关闭时整块不输出 ──────────────────────────────
*
* 以前这里会渲染一个 `.comments` 容器,并在里面打一句
* 「评论已关闭。」/「评论已关闭,但你可以阅读上面的讨论。」
* —— 用户明确要求:**评论一关,这一块整个消失**,
* 不留提示文字、不留空容器、已有评论也不再显示。
* 正文结束之后直接就是页脚。
*
* 🔴 为什么是 `return` 而不是把内容包进 `if (comments_open())`:
* 包起来的话,容器 `<div class="comments">` 仍会被输出(一个空盒子),
* 而主题给 `.comments` 有 margin/padding,页面上会留下一道空白 ——
* 看起来像"有个东西没加载出来"。`return` 是**一个字节都不输出**,
* 这才是需求要的"什么都没有"。
*
* 🔴 必须放在**任何输出之前**:这个位置在文件里还没有 echo 过任何东西,
* 所以 return 是干净的。一旦挪到后面,前面的 HTML 已经发出去了,
* 再加 return 只会得到一个残疾的残块。
*
* ⚠️ `comments.php` 是被 `comments_template()` include 的,
* 在它里面 `return` 只会结束这个模板文件的执行,
* 不会中断 single.php / page.php 的后续渲染。
*/
if (!comments_open()) {
return;
}
require get_template_directory() . '/pages/page-smilies.php';
$yang_post_id = get_the_ID();
$yang_comment_count = (int) get_comments_number($yang_post_id);
$yang_max_depth = (int) get_option('thread_comments_depth', 5);
if ($yang_max_depth <= 0) {
$yang_max_depth = 5;
}
$yang_threaded = (bool) get_option('thread_comments');
$yang_has_comments = ($yang_comment_count > 0);
/* 评论是否分页:分页时首屏只给「加载更多」,不一次性铺全部 */
$yang_paged = (bool) get_option('page_comments');
$yang_per_page = (int) get_option('comments_per_page', 20);
if ($yang_per_page <= 0) {
$yang_per_page = 20;
}
?>
<div class="comments" id="comments">
<div class="comments-head">
<h3 class="title">
<?php
if ($yang_comment_count > 0) {
printf(
/* translators: %s: 评论条数 */
esc_html__('评论(%s)', 'kratos'),
esc_html(number_format_i18n($yang_comment_count))
);
} else {
esc_html_e('评论', 'kratos');
}
?>
</h3>
<?php
/* 3.2.46:原本是 `if (comments_open())`。文件开头已在评论关闭时
return,所以这里恒真,条件已去掉。留着它会让后来人以为
"还另有一个关评论的分支"。 */
?>
<a class="comments-jump-form" href="#respond"><?php esc_html_e('写下你的评论', 'kratos'); ?></a>
</div>
<?php
/* 3.2.46 起这里不再有「评论已关闭」提示分支:
文件开头已经 `if (!comments_open()) return;`,走不到这一行。
原先两句「评论已关闭。」「评论已关闭,但你可以阅读上面的讨论。」
是用户明确要求删掉的 —— 关评论就该整块消失。 */
?>
<?php if ($yang_has_comments) : ?>
<?php
/* <ol> 而不是 <div>:<li> 的父元素必须是列表容器。
加 reversed/start 会打乱视觉顺序,所以只用普通 <ol>。 */
?>
<ol class="yang-comment-list" id="yang-comment-list">
<?php
/* 取数自己来,不让 wp_list_comments 去推导分页。
原因见 inc/theme-comments.php 里 yang_comment_page() 的注释:
当它推导出的当前页与传入的 page 相等时,内部 $_comments
不会被赋值,列表直接是空的。显式传数组就绕开了那段逻辑。 */
$yang_page_comments = yang_comment_page(
$yang_post_id,
1,
$yang_paged ? $yang_per_page : 0
);
wp_list_comments(array(
'type' => 'comment',
'callback' => 'comment_callbacks',
'walker' => new Yang_Walker_Comment(),
'style' => 'ol',
'max_depth' => $yang_threaded ? $yang_max_depth : 1,
'avatar_size' => 48,
'per_page' => 0,
'page' => 0,
), $yang_page_comments);
?>
</ol>
<?php
/* 加载更多:只在「有分页」且「顶层评论数超过一页」时出现。
判据必须与 AJAX 的 has_more 同口径(都按顶层算),
否则会出现"按钮还在、点了却什么都没有"。
旧模板无条件输出,即使只有 1 条评论也显示一个点不动的按钮。 */
$yang_top_count = (int) get_comments(array(
'post_id' => $yang_post_id,
'status' => 'approve',
'parent' => 0,
'count' => true,
));
$yang_has_more = $yang_paged && $yang_top_count > $yang_per_page;
?>
<div class="comments-more" id="yang-comments-more"<?php echo $yang_has_more ? '' : ' hidden'; ?>>
<button type="button" class="comments-more-btn" data-page="2">
<?php esc_html_e('加载更多评论', 'kratos'); ?>
</button>
</div>
<?php
/* 3.2.46:这里原本是 `elseif (comments_open())`。文件开头已经
在评论关闭时 `return`,所以走到这里时 comments_open() **恒真** ——
保留那个条件只会变成一句永远为真的死判断,后来人读到这里会
以为"还有个关评论的分支要照顾"。改成 else。 */
?>
<?php else : ?>
<p class="comments-empty"><?php esc_html_e('还没有评论,来说第一句吧。', 'kratos'); ?></p>
<?php endif; ?>
<?php
/* 3.2.46:原本是 `if (comments_open())`,同上,恒真,已去掉条件。
下面的登录提示 / 评论表单因此只在"评论开放"时才走到这里 ——
这正是需求要的:关评论时整块不输出(而且文件开头就 return 了,
连外层 .comments 容器都不会有)。 */
?>
<?php if (get_option('comment_registration') && !is_user_logged_in()) : ?>
<div id="respond" class="comment-respond">
<p class="comments-login-required">
<?php
printf(
/* translators: %s: 登录链接 */
wp_kses(
__('您需要 <a href="%s">登录</a> 之后才可以评论。', 'kratos'),
array('a' => array('href' => array()))
),
esc_url(wp_login_url(get_permalink()))
);
?>
</p>
</div>
<?php else : ?>
<div id="respond" class="comment-respond mt-4">
<h3 class="comment-reply-title">
<span id="yang-reply-title-text"><?php esc_html_e('发表评论', 'kratos'); ?></span>
<?php
/* 取消回复按钮:由 JS 控制显示,模板里先藏起来。
不用 WordPress 的 cancel_comment_reply_link():
它生成的 <a href> 依赖 JS 拦截,没 JS 时会跳到
?replytocom= 的地址,行为很怪。这里用 button。 */
?>
<button type="button" id="yang-cancel-reply" class="comment-cancel-reply" hidden>
<?php esc_html_e('取消回复', 'kratos'); ?>
</button>
</h3>
<?php
/* 正在回复谁:JS 更新这段文字。
放在表单上方而不是按钮旁边,是因为表单会被"搬"到
被回复评论的下方,粘在按钮上的提示会跟着乱跑。 */
?>
<p class="comment-reply-hint" id="yang-reply-hint" hidden></p>
<form id="commentform" name="commentform" class="yang-comment-form" method="post"
action="<?php echo esc_url(site_url('/wp-comments-post.php')); ?>">
<?php if (!is_user_logged_in()) : ?>
<?php
/* 已登录用户的昵称邮箱从 cookie 里取,未登录才显示输入框。
esc_attr 必须做:cookie 里的值是用户可控的。 */
$yang_commenter = wp_get_current_commenter();
?>
<div class="comment-info">
<div class="comment-field comment-form-author">
<label for="author" class="sr-only"><?php esc_html_e('昵称', 'kratos'); ?></label>
<span class="comment-field-icon" aria-hidden="true"><i class="kicon i-user"></i></span>
<input class="comment-input" id="author" name="author" type="text"
placeholder="<?php esc_attr_e('昵称(必填)', 'kratos'); ?>"
value="<?php echo esc_attr($yang_commenter['comment_author']); ?>"
required="required" autocomplete="nickname" maxlength="60">
</div>
<div class="comment-field comment-form-email">
<label for="email" class="sr-only"><?php esc_html_e('邮箱', 'kratos'); ?></label>
<span class="comment-field-icon" aria-hidden="true"><i class="kicon i-cemail"></i></span>
<input class="comment-input" id="email" name="email" type="email"
placeholder="<?php esc_attr_e('邮箱(必填,不会公开)', 'kratos'); ?>"
value="<?php echo esc_attr($yang_commenter['comment_author_email']); ?>"
required="required" autocomplete="email" maxlength="120">
</div>
<div class="comment-field comment-form-url">
<label for="url" class="sr-only"><?php esc_html_e('网址', 'kratos'); ?></label>
<span class="comment-field-icon" aria-hidden="true"><i class="kicon i-url"></i></span>
<input class="comment-input" id="url" name="url" type="url"
placeholder="<?php esc_attr_e('网址(选填)', 'kratos'); ?>"
value="<?php echo esc_attr($yang_commenter['comment_author_url']); ?>"
autocomplete="url" maxlength="200">
</div>
</div>
<?php endif; ?>
<div class="comment-textarea">
<label for="comment" class="sr-only"><?php esc_html_e('评论内容', 'kratos'); ?></label>
<?php
/* ── 3.2.24 富文本工具条 ──────────────────────────────
* 在输入框上方一排小按钮:加粗 / 斜体 / 字体颜色 / 表情。
* 不是 contenteditable 富文本 —— 评论内容仍是纯文本存储,
* 工具条只是在光标处插入受控的少量 HTML 标签
* (<strong>/<em>/<span style="color:…">),后端由
* theme-comments.php 里的 kses 白名单放行这三种标签,
* style 属性由 WP 核心的 safecss_filter_attr 二次消毒,
* 只放行 color 这类安全属性。改动面最小、无 XSS 面。
*
* 表情按钮原来在底栏左侧,3.2.24 起并入
* 工具条;id(yang-smile-btn / yang-smile-panel)不变,
* comments.js 的面板开合逻辑不用改。
*/
?>
<div class="comment-toolbar" role="toolbar"
aria-label="<?php esc_attr_e('评论格式工具条', 'kratos'); ?>">
<button type="button" class="comment-tool-btn" id="yang-tool-bold"
title="<?php esc_attr_e('加粗', 'kratos'); ?>"
aria-label="<?php esc_attr_e('加粗', 'kratos'); ?>">
<span class="comment-tool-glyph comment-tool-glyph-bold" aria-hidden="true">B</span>
</button>
<button type="button" class="comment-tool-btn" id="yang-tool-italic"
title="<?php esc_attr_e('斜体', 'kratos'); ?>"
aria-label="<?php esc_attr_e('斜体', 'kratos'); ?>">
<span class="comment-tool-glyph comment-tool-glyph-italic" aria-hidden="true">I</span>
</button>
<span class="comment-toolbar-sep" aria-hidden="true"></span>
<div class="comment-tool-group">
<button type="button" class="comment-tool-btn comment-tool-color" id="yang-tool-color"
aria-expanded="false" aria-controls="yang-color-panel"
title="<?php esc_attr_e('字体颜色', 'kratos'); ?>"
aria-label="<?php esc_attr_e('字体颜色', 'kratos'); ?>">
<span class="comment-tool-glyph comment-tool-glyph-color" aria-hidden="true">A</span>
</button>
<div class="comment-color-panel" id="yang-color-panel" hidden>
<?php
/* 预设色板:常用 8 色。data-color 由 JS 读去包选区,
不用 <input type=color> —— 原生取色器弹窗体验割裂,
而且访客只是想快速标个重点色。 */
$yang_tool_colors = array(
'#000000' => '黑色',
'#7f8c8d' => '灰色',
'#e74c3c' => '红色',
'#e67e22' => '橙色',
'#f1c40f' => '黄色',
'#2ecc71' => '绿色',
'#00a2ff' => '蓝色',
'#9b59b6' => '紫色',
);
foreach ($yang_tool_colors as $yang_c => $yang_name) :
?>
<button type="button" class="comment-color-swatch"
data-color="<?php echo esc_attr($yang_c); ?>"
style="background-color: <?php echo esc_attr($yang_c); ?>;"
title="<?php echo esc_attr(sprintf(__('字体颜色:%s', 'kratos'), $yang_name)); ?>"
aria-label="<?php echo esc_attr(sprintf(__('字体颜色:%s', 'kratos'), $yang_name)); ?>"></button>
<?php endforeach; ?>
</div>
</div>
<span class="comment-toolbar-sep" aria-hidden="true"></span>
<div class="comment-tool-group comment-tool-group-smile">
<button type="button" class="comment-tool-btn comment-smile-btn" id="yang-smile-btn"
aria-expanded="false" aria-controls="yang-smile-panel"
title="<?php esc_attr_e('插入表情', 'kratos'); ?>"
aria-label="<?php esc_attr_e('插入表情', 'kratos'); ?>">
<i class="kicon i-face" aria-hidden="true"></i>
</button>
<div class="comment-smile-panel" id="yang-smile-panel" hidden>
<?php echo $smilies; ?>
</div>
</div>
<span class="comment-toolbar-sep" aria-hidden="true"></span>
<?php
/* ── 3.2.42 代码块按钮 ────────────────────────────
* 访客讨论文章正文里的代码时,往往需要贴一段代码回来。
* 在此之前这件事**根本做不到**(用真实 WP 7.1.3 实测):
* · `<pre>` 不在 kses 白名单里 → 整条被剥掉
* · `<code>` 在,但任何属性都会被剥,包括 class →
* 语言标记保不住
* · 没有 pre 兜着时 wpautop 会套 <p>,
* 而 <p> 的 white-space:normal 把行首缩进折叠掉
* · wptexturize 把 ' 变成弯引号、-- 变成破折号,
* make_clickable 把 URL 包成 <a> —— 代码直接跑不了
* 现在 pre 已进白名单,后两条会被 pre 自动挡住
* (实测 11 条判据全通过)。
*
* 语言必须让访客**自己选**:不选就没法可靠地上色,
* 而自动识别在站内实测只有 22% 准确率,不能替访客决定。
*/
$yang_hl_choices = function_exists('yang_hl_themes') ? array(
'text' => __('纯文本', 'kratos'),
'bash' => 'Bash',
'shell' => 'Shell',
'python' => 'Python',
'java' => 'Java',
'javascript' => 'JavaScript',
'typescript' => 'TypeScript',
'php' => 'PHP',
'c' => 'C',
'cpp' => 'C++',
'csharp' => 'C#',
'go' => 'Go',
'rust' => 'Rust',
'ruby' => 'Ruby',
'kotlin' => 'Kotlin',
'swift' => 'Swift',
'sql' => 'SQL',
'json' => 'JSON',
'yaml' => 'YAML',
'xml' => 'HTML/XML',
'css' => 'CSS',
'diff' => 'Diff',
'markdown' => 'Markdown',
'makefile' => 'Makefile',
'powershell' => 'PowerShell',
'dos' => 'Batch',
'ini' => 'INI',
'dockerfile' => 'Dockerfile',
'nginx' => 'Nginx',
) : array('text' => __('纯文本', 'kratos'));
?>
<div class="comment-tool-group comment-tool-group-code">
<button type="button" class="comment-tool-btn comment-code-btn" id="yang-code-btn"
aria-expanded="false" aria-controls="yang-code-panel"
title="<?php esc_attr_e('插入代码', 'kratos'); ?>"
aria-label="<?php esc_attr_e('插入代码', 'kratos'); ?>">
<span class="comment-tool-glyph comment-tool-glyph-code" aria-hidden="true">&lt;/&gt;</span>
</button>
<div class="comment-code-panel" id="yang-code-panel" hidden>
<div class="comment-code-panel-title"><?php esc_html_e('选择代码语言', 'kratos'); ?></div>
<div class="comment-code-langs">
<?php foreach ($yang_hl_choices as $yang_v => $yang_n) : ?>
<button type="button" class="comment-code-lang"
data-lang="<?php echo esc_attr($yang_v); ?>"><?php echo esc_html($yang_n); ?></button>
<?php endforeach; ?>
</div>
<div class="comment-code-panel-foot">
<?php esc_html_e('选好后在光标处插入代码块,把示例文字换成你的代码即可。', 'kratos'); ?>
</div>
</div>
</div>
</div>
<textarea class="comment-input" id="comment" name="comment" rows="6"
hidden aria-hidden="true" tabindex="-1"></textarea>
<?php /* 3.2.25 所见即所得编辑区:格式/表情所见即所得,
不再往框里插 <strong> 这类源码。真实内容提交前由
comments.js 序列化进上面隐藏的 #comment。
required 不能放隐藏 textarea 上(display:none 的
控件无法聚焦,原生校验会报错),空值检查在 JS 做。 */ ?>
<div class="comment-editor" id="yang-editor" contenteditable="true"
role="textbox" aria-multiline="true"
aria-label="<?php esc_attr_e('评论内容', 'kratos'); ?>"
data-placeholder="<?php esc_attr_e('说点什么…(加粗、颜色、表情点上方按钮)', 'kratos'); ?>"></div>
<div class="comment-bar">
<div class="comment-bar-right">
<span class="comment-status" id="yang-comment-status" role="status" aria-live="polite"></span>
<button type="submit" id="submit" class="comment-submit">
<?php esc_html_e('提交评论', 'kratos'); ?>
</button>
</div>
</div>
</div>
<?php
/* honeypot:把这个字段留空的是人,填了的是机器人。
CSS 用 position:absolute 移出视口,不能用 display:none
—— 部分爬虫会跳过隐藏字段,但多数不会跳过"移出视口"的。 */
?>
<p class="comment-hp" aria-hidden="true">
<label for="yang_hp"><?php esc_html_e('请勿填写此项', 'kratos'); ?></label>
<input type="text" id="yang_hp" name="yang_hp" value="" tabindex="-1" autocomplete="off">
</p>
<?php
wp_nonce_field('yang_comment_submit', 'yang_comment_nonce');
comment_id_fields();
do_action('comment_form', $yang_post_id);
?>
</form>
</div>
<?php endif; ?>
<?php /* 3.2.46:原来这里还有一个 `endif;` 关掉外层的
`if (comments_open())`。那个条件已删除,所以这里也少了对应的
endif —— 多的那个会导致 "syntax error"。 */ ?>
</div>