Commit Graph
10 Commits
Author SHA1 Message Date
laoyang f5fd363dce fix(widgets): 分类挂件改成「点开才占一行」的下拉面板 #24
需求改判:3.2.54 做的「父级常显 + 子级折叠树」被否,用户要的是
「不点只有一行,点开把所有一级分类拉出来」的真下拉面板。

## 改了什么

- widget() 重写:`<details class="yang-cats-drop">` + `<summary>` 触发条
  + `position:absolute` 浮层面板。收起时整个挂件只占一行(40.5px),
  展开时面板盖在下方内容上,不把后面的挂件推走。
- children 兜底值 true → false。线上实例的配置里本来没有这个键,
  所以走 pick() 兜底 ⇒ 改兜底就等于改线上行为,用户零操作即变成
  「只列一级分类」,不用进后台勾一次。
- 子级不再嵌套 <details>(3.2.54 是嵌套)。现在父级与子级的 DOM
  结构完全一致,3.2.54 那个「space-between 推不动、数字右边界差
  7.1px」的缺陷从结构上消失了,不需要 flex:1 补丁。
- 补一段渐进增强脚本,只做「点外部收起 / Esc 收起并还焦点」两件事,
  不接管开合本身(关掉 JS 仍能正常展开跳转)。
- CSS:.w-cats 区块整块重写。修掉子级 li 右内边距叠加的缺陷 ——
  子级 ul 嵌在已 padding:0 12px 的父级 li 里,子级 li 又写了一遍
  右内边距 ⇒ 叠加成 24px,导致子级数字比父级多缩进 12px、父子排不成
  一列,竖着比大小时会错位。改成 0 后父子数字同列。
- style.css 版本号 3.2.54 → 3.2.55(3.2.54 已落过 update.json,
  同号重发会让 WP 的 version_compare 判「没有新版本」)。

## 验证

- 端到端(线上真实 WordPress,96 项):全通过
- 反证:内容判据 12 组 + 端到端 3 组,全部真红,无一组空跑
- 视觉几何(真实浏览器 50 项):全通过
- 上一版(3.2.53)老代码跑同一套端到端判据:88 项失败
- 包内 PHP lint 96 个全过;zip 无 1980 时间戳(防 OPcache 误判)

## 发布

只写 theme-update/:zip + readme + update.json → 3.2.55。
线上主题目录仍是 3.2.54,等用户在后台点升级(红线)。
2026-10-27 13:00:37 +08:00
laoyang 4c1d67b86b 3.2.54 主题接管「文章分类」挂件:父子折叠树替代核心下拉框(#24)
原来侧栏那个分类挂件是 WP 核心的 WP_Widget_Categories,实例配置
dropdown=1,输出的是 <select> 下拉框。主题 CSS 里那套
`.widget_categories ul li` 只匹配 ul li,下拉框里没有 ul
⇒ 写了但一条都没生效。不是"没写样式",是样式和渲染形态对不上。

做法:沿用核心的 id_base `categories` 直接顶替(不是新建独立挂件)。
- option 名不变,_register_widgets() 照旧认领存量实例 categories-3
- 用户升级后什么都不用做:就地变新样式、位置不变、标题沿用
- 代价:update() 需容忍核心的 dropdown/hierarchical 老字段
  (不读也不显式删,用户重存表单时自然收敛掉)

呈现:父级常显 + 子级可折叠,用 <details>/<summary> 原生实现,零 JS。
父级链接放在 <summary> 里不会误触折叠 —— HTML 规范规定 <summary>
的"激活即折叠"只在点击目标非 interactive content 时触发。

显式 pad_counts:核心是在 show_count && hierarchical 时隐式打开的
(category-template.php:392)。不显式给的话父级数字 220→188,
用户会以为丢了 32 篇文章。

orderby=count 不交给 SQL:WP_Term_Query 翻成 ORDER BY tt.count
(原始值),而 pad_counts 是查询后由 PHP 补的 ⇒ 数字大的排在小的
后面。改成本地取回后按补过的 count 排,并做 strnatcasecmp tie-break。

分类名不 decode:实测 esc_html() 对 & 的输出恒为 &amp;,所以库里
存 & 还是存 &amp;,最终 HTML 字节一致、浏览器显示也一致。decode
是冗余且有多一层绕过风险。(建单时判断错了,此处自我纠正。)

删掉失效的 .widget_categories ul li 整套规则及 @media 里的宽度规则。

样式三个缺陷(线上真实页面量坐标才发现,不是看代码能看出来的):
1. 主题全局 ul>li{margin-bottom:6px;padding-left:2px;list-style:circle}
   (kratos.css:74) 未被重置 ⇒ 每行多 6px,8 行累积 42px 行距发虚
2. 同上 padding-left:2px 只覆盖了 :not(.has-children) ⇒ 带箭头行错位 2px
3. 有子级行的 li 里只有 <details> 一个 flex 子项,space-between 推不动;
   <details> 默认按内容收缩 ⇒ 数字右边界差 7.1px(实测
   [220.5,220.5,227.6,227.6,220.5])

验证:
- 端到端(真实 WP 加载待测副本)77 通过 / 0 失败
- 反证 12 组全部命中预期判据,无一组空跑
- CSS 视觉反向验证 3 组:三处缺陷逐个注回,全部被量出来
- 视觉几何 18 项全绿(行距/胶囊/对齐/留白/悬停/箭头)
- 用 3.2.53 老代码跑同一套判据:11 通过 / 57 失败

过程中修掉 5 条假判据(全在我自己的测试脚本里)。用老代码跑时它
居然"通过 16 条",逐条查全是假绿,根因同一个:否定式断言作用在
空靶上(与 pitfalls #424 同一族)。最典型的一条:
  /@media[^{]*\{[^}]*widget_categories/ 的 [^}]* 遇到块内第一个 }
  就截断 ⇒ 永远匹配不到 ⇒ 恒真。老代码 2354 行明明有那条规则。
改成按花括号配对切块后,老代码从「16 通过/50 失败」变「11/57」。

包 kratos_yang-3.2.54.zip:4783259 字节 / 237 成员 / 0 个 1980 时间戳
readme 45 个版本标题 · update.json → 3.2.54
服务器主题目录仍 3.2.53,未被覆盖
2026-10-27 12:18:53 +08:00
laoyang 75848f2ba2 3.2.53 后台小工具页恢复传统拖放面板(#23)
现象:用户在后台「外观 → 小工具」找不到可以拖动挂件的地方,看到的全是区块。

根因:WP 5.8 起后台小工具页默认走区块编辑器
(wp-admin/widgets-form-blocks.php),它没有「从左栏拖到右栏」这个动作,
只能点「+」再搜名字。本主题此前从未关掉它。

改动:
1. 关闭区块小工具编辑器,两道开关互为备份
   - add_filter('use_widgets_block_editor', '__return_false')
   - remove_theme_support('widgets-block-editor'),挂 after_setup_theme:20
     (核心 wp_setup_widgets_block_editor 挂在 priority 1 默认 add,
       所以 remove 必须晚于它,否则静默失败)
2. 4 个挂件(ad/about/tags/posts)构造函数的名字改为显式传第 2 参

关于第 2 点的性质说明(重要,避免误读成"修了缺陷"):
原先写 parent::__construct(false, false, $widget_ops) 时,
$widget->name 确实是 false,但后台列表/搜索读的**不是**它,而是
$wp_registered_widgets[...]['name']。后者经
wp_register_sidebar_widget() 里的 array_merge($widget, $options)
被 $widget_ops['name'] 覆盖成真名字。
实测(老代码):$widget->name = false,
$wp_registered_widgets['ad-1']['name'] = '图片广告'。
所以挂件名字从来没空过,用户找不到挂件与名字无关,纯粹是区块编辑器没有拖放。
第 2 点属于可读性清理(不再依赖隐式覆盖),不是用户可见缺陷的修复。

id_base 全部保持原值(ad/about/tags/posts),已有挂件实例不会丢。

验证:
- 真实 WP 7.1.3 加载待发布包,页面级 28 条 + 元数据 50 条判据全绿
- 老代码跑同一套判据:仅区块开关相关失败,名字类判据全部通过(印证上述判断)
- 9 组反证全部真红(含「只去掉一道开关仍应全绿」的冗余性验证)
- 96 个 PHP 文件全量 lint 通过
2026-10-27 09:21:02 +08:00
laoyang baa1d16fd1 3.2.52: 新增「最近评论」挂件(替代 Comments Widget Plus 插件)#22
主题自带一个好看的最近评论挂件,用户不必再装插件。

新挂件 widget_recent_comments(id_base = yang_recent_comments):
- 头像 40px 圆形 + object-fit:cover,带 max-width/max-height 双保险
- 摘要按字符截断(mb_substr),先剥标签再压空白,不走 wp_html_excerpt(按字节切半个汉字)
- 作者 · 相对时间 · 文章标题三段各自成 span,不拼 i18n 句式
- 三层字号:摘要 15px / 作者 14px / 文章 13px(文章字号继承自 meta)
- 条间虚线,最后一条去掉
- 后台九项设置:标题、条数、头像、头像尺寸、摘要、摘要字数、排除 pingback、评论类型、文章类型

实现要点:
- CSS 不依赖 .k-main .sidebar 祖先,只依到 .w-comments 自身
  (3.2.51 的红线:带祖先的选择器在后台「外观→小工具」预览里一条不生效)
- .w-comments 自己不写 padding,交给 widget 基类,避免与其它挂件错位
- 只查 status=approve 且 post_status=publish,草稿文章评论不露出

同时修一个前后台不一致的 bug:
- form() 的 post_type 原来走 pick(),pick() 把空串当"没填"回落 'post'
  ⇒ 老实例(存空串)在后台下拉框显示「文章」,前台却按「全部类型」渲染。
  改为 widget() 与 form() 都直接读原值,两处都识别空串/any 为「全部类型」。

验证:行为 112/0 · 行为反证 25/25 + CSS 10/10 · 几何 50/0 ·
线上真实页面 30/0 · 静态反证 23/0 · 发布脚本 22 组反证全真红。
2026-10-27 08:40:53 +08:00
laoyang c1dde099d7 「个人简介」挂件:头像/昵称/简介/背景四项可单独填 + 修大图撑爆 #21
一、头像 / 昵称 / 简介原来**只读主题设置**,挂件表单里没有字段
  · 用户点开侧栏「个人简介」只能改背景图,头像和那句话得跑去
    「主题设置 → 站长信息」改全站共用的那份。
  · 现在四项都是挂件实例字段:avatar / nickname / introduce / background。
  · 取值顺序:实例 → 主题设置 → 内置兜底,**留空即回落**,
    老站点升级后显示一点不变。
  · pick() 判空统一用「trim 后是不是空串」,不用 empty()
    (!empty('0') 是 false,昵称就叫「0」会被当没填)。
  · 简介用 sanitize_textarea_field(保留换行),URL 用 esc_url_raw
    (不做 HTML 实体化,避免与 esc_attr 双重转义)。

二、后台 form() 补齐五个字段
  · 头像 / 背景各带「选择图片」按钮(调媒体库),背景另有「清除」。
  · 输入框显示用户填的原始值,缩略图显示前台最终会用的那张,刻意分开。
  · 资源加载统一到 yang_widget_admin_assets(),不再每个挂件各挂一次。

三、大图撑爆挂件的真因:CSS 选择器依赖三层祖先
  · 老规则是 `.k-main .sidebar .w-about .wrapper img`,而后台
    「外观 → 小工具」预览里没有 `.k-main .sidebar` 这层祖先,
    规则一条都不生效 ⇒ 头像按原图 2136×2136 铺开、背景塌成 0。
    前台看着正常是因为前台恰好有那层祖先,问题被完全掩盖。
  · 改为只管 `.w-about` 内部元素,并补 max-width/max-height 双保险。
  · 老规则必须**删掉**而不是并存:它是 (0,3,1),新规则是 (0,2,1),
    并存时老规则在前台永远赢,新规则只对后台生效。
  · 保留 `.k-main .sidebar .w-about { padding:0 }`:通用规则
    `.k-main .sidebar .widget` 是 (0,3,0),少了这条会多出内边距、整体高 57px。
  · 头像保持 inline(display:block 会让它脱离 text-align:center 的居中)。
  · 顺带:object-fit 从 fill 改 cover(超宽图不再压扁);
    背景 url() 补引号(文件名带空格时不再整条声明作废)。

验证
  · 行为测试 67 条全通过;反向验证 13 组全部真红。
  · 浏览器几何 76 条全通过(前台/后台 × 四种极端原图)。
  · 修复前 CSS 跑同一套判据:38 条报红,精确复现用户症状
    (后台挂件高 2217、头像 2136×2136、横向滚动 2146)。
  · 线上真实页面 + 新版 CSS:16 条全通过。
2026-10-26 23:51:20 +08:00
laoyang 8367231e91 侧栏挂件一致性:标题可配、标签列数、重复 id、URL 转义 (3.2.50) #20
一并修掉上一轮列出的 5 处侧栏挂件遗留问题:

1. 标题不读 $args(4 个挂件标题在后台改不了)
   新增 yang_widget_title($args, $instance, $fallback, $id_base),5 个挂件
   统一走它:读 $instance['title'] → 空则回落 $fallback → 过 widget_title
   过滤器 → 输出用 $args['before_title']/after_title → 仍为空则整个节点不输出。
   四个挂件的 update() 补 title,form() 补输入框 + 「留空则不显示标题」说明。
   widget_tags 留空回落「标签聚合」,老站点显示不变。

2. 标签列数随屏宽反转(1440 一列、1180 反而两列)
   逐视口实测:真实断点在 1311→1310,@media(max-width:1310px) 只把格子压成
   110px 而不改侧栏宽度(侧栏 992px 以上恒为容器 1/3),于是格子变小反而能
   塞下 3 个。改为 .w-tags .item a / .widget_links ul li 用
   width: calc(50% - 2px) + 父容器 font-size:0(消掉 inline-block 之间的
   空白宽度),并在 1310 断点里删掉这两条 110px 规则(分类/归档的 110px
   不是网格,保留)。判据守的是「列数单调不增」而不是「某视口恰好两列」。

3. 重复 id nav-tab(桌面一份、手机一份)
   两份导航合并为一份,id 全部带实例后缀:yang_widget_uid($this) 取
   $widget->id(如 widget_posts-2),输出 {uid}-tab / {uid}-new 等,
   aria-controls / aria-labelledby 同步指向真实 id,两实例并用也不撞。

4. 搜索框固定 id(searchform / search / searchsubmit)
   同样改为 {uid}-form / {uid}-field / {uid}-submit,并补
   <label class="sr-only" for="{uid}-field">搜索</label>。

5. 图片 URL 未加引号 + 未转义
   src/href 一律加引号 + esc_url(),alt 用 wp_strip_all_tags + esc_attr,
   简介用 nl2br(esc_html())。背景图踩了一个自己写出来的坑:一开始写成
   esc_url() 套在 esc_attr() 里 = 双重转义,& 变 &amp;amp;,CSS 里拿到
   字面量 amp,背景图 404。改为 url() 内部用 esc_url_raw()(只做协议/字符
   清洗,不入实体),整个 style 属性交给 esc_attr() 统一转义一次。

顺带把侧栏标签页图标在窄屏隐藏(<1200px),避免三个 tab 挤在一起。

验证:
- _widgets_consistency.php        68 通过 / 0 失败
- _widgets_consistency_falsify.php 24 通过 / 0 失败(基线1·自检3·注入20)
- _widgets_hot_test.php           33 / 0
- _widgets_hot_falsify.php        20 / 0
- _widgets_hot_geom.js            24 / 0(真实浏览器几何)
- _widgets_ids_dom.js              5 / 0(真实 DOM 查重复 id)
- _widgets_visual20.js            30 / 0(像素级:图片 naturalWidth>0、
                                      标题装饰线、标签等宽、tab 点击)
- _widgets_icon_wide.js            7 / 0(断点精确在 1200)
- _widgets_tags_geom.js            5 / 0(列数单调性)
- 发布脚本 R1~R10 十组注入全部真红
2026-10-26 23:04:28 +08:00
laoyang d3070bea5b 3.2.49: 修「文章聚合→热点」只显示一条(换全站累计热度算法) #19
根因不是排序,是候选池:旧 SQL 先圈「最近 30 天发布」再按评论排序取 6。
本站实测 30 天只有 1 篇 post(7 天 0 / 90 天 1 / 180 天 6 / 365 天 15 / 全站 967),
候选池只有 1 条,LIMIT 6 也只能返回 1 条。

改成全站累计热度,三项取对数加权:
  3*log10(views+1) + 5*log10(love+1) + 4*log10(comments+1)
· 取 log10:views 中位 4981/最大 56075,而 love 最大 40、评论最大 93,
  不压缩则 views 一项吃掉全部权重。
· 权重 3/5/4(不是通用 1/3/5):用通用值实测前 8 名 6 篇是 2009 年老随笔
  —— 评论 1.97*5=9.86 > views 4.75*1=4.75,退化成「评论数排行榜」。
· 有意不加时间衰减:HN/Reddit 那套是「新内容榜」,会把老文压到看不见,
  正好回到原问题。要时效榜是另一个功能。
· 仍限定 post_type='post'(用户要求只看文章,不看 page;
  ID 217「留言本」/522「博客时间」核查过都是 page,天然被挡掉)。

顺手修掉三处旧问题:
· 删 date_default_timezone_set("PRC") —— 它会改掉整个 PHP 请求的全局时区
· 标题进属性改 esc_attr()(旧代码裸拼,含双引号会截断 title 属性)
· get_posts 参数从 'numberposts=6 & offset=0' 字符串改成数组
  (字符串里那个空格让参数名变成 ' offset',offset=0 实际从未生效)

后台:「统计天数」输入框下线(它控制的就是那个缩死候选池的时间窗),
update() 不再保存 days,老实例残留值下次保存时丢掉。

验证:
· 主测试 33/0(分数用独立实现重算比对,不调用产品函数算期望值)
· 反向验证 20/0(基线 2 + 框架自检 2 + 注入 8 组×2)
  八组注入:时间窗加回 / 去 post_type / 去 log10 / 权重改 1-3-5 /
  标题退裸拼 / 全局时区加回 / 排序改升序 / 后台加回统计天数
· 真浏览器几何 22/0(1440+1180 两视口):6 条真画出来、宽度等于面板、
  图标存在、不重叠、长标题走省略号不溢出
· 发布反证 8/8、线上包校验 10/10
2026-10-26 22:30:02 +08:00
laoyang ef9a58dc51 v3.2.13 图片选项全部自带默认图,新装站点不再出现空洞/破图
根本原因:kratos_option() 运行时只读数据库,`std` 仅在后台表单预填和
「恢复默认」时生效,前台根本读不到。因此新装站点所有图片项都是空值。
另外 isset() 挡不住「用户清空」——of_sanitize_upload() 对空输入返回 '',
键存在且值为空串,调用方传的兜底值被绕过。

同时不能把绝对 URL 写进 std:它会随保存一起写库,主题目录改名/换 CDN/
迁站后永久 404。本站 s_wechat_url / g_donate_alipay / g_donate_wechat
三项就指向已删除的 kratos-master 目录,一直在破图(这三项是 hidden 字段,
后台界面上看不到,所以坏了很久没人发现)。

做法:
- 新增 inc/theme-option-defaults.php,图片默认值注册表(11 项),
  默认图一律运行时给出,不写数据库。
- yang_option_image() 三层判断:空/纯空白 → 默认图;指向已不存在的主题
  目录 → 默认图;文件不存在则不输出地址(避免换来一个 404)。
- 陈旧判断用 get_template()(主题 slug)而非 basename(目录名),避免本地
  开发目录名与 slug 不一致时误伤当前主题自己的图。
- 模板统一走取值函数:header(logo/favicon/banner)、footer(微信码/国旗)、
  404、index、article 缩略图、widgets 头像、page-toolbar、theme-core 赞赏码。
- 国旗与 favicon 的渲染抽成 yang_flag_block() / yang_favicon_tag(),
  空值时不输出空 src / 空 href。
- 后台 9 处 std 改为 yang_options_default_img(),与前台默认图同源;
  该包装函数自带 function_exists 防护,单独加载 theme-options.php 不会 Fatal。

新增图片(本地生成,合计 291KB):default-404 / default-share /
default-empty / default-avatar / default-banner / default-flag / favicon.ico。
favicon 为 WordPress 风格自绘(品牌蓝圆底 + 白色 W),非站长个人专用那张。

验证:run_tests 146 条(新增第 4 组图片默认值 22+ 条);falsify 35 条注入
全部能被抓住(新增 10 条);verify_all.sh 14 组全绿。
2026-10-23 19:50:07 +08:00
laoyang 4f0d19fb91 版本号 3.2.4 -> 3.2.5(修 Prism 报错,项目地址改为自建 Git)
## 修 Prism is not defined

首页控制台报 `Uncaught ReferenceError: Prism is not defined`,来源是
wp-editormd 插件(不是本主题):它在 Prism 脚本加载之前就无条件执行了
`Prism.plugins.autoloader.languages_path = "..."`。

线上页面里的真实顺序:
    第 668 行  该赋值语句
    第 695 行  prism-core.min.js
    第 696 行  prism-autoloader.min.js

在 `wp_head` 优先级 0 垫一个最小空壳 `window.Prism`(含 plugins.autoloader
这条访问链路),让那句赋值有地方可写。已核对 prism-core.min.js 源码:它开头
只读 `u.Prism.manual` 与 `u.Prism.disableWorkerMessageHandler`,结尾
`global.Prism = Prism` 整体覆盖 —— 空壳必定被真实对象替换,不承担持久职责,
只为吸收那条过早的赋值。已用 jsdom 验证:去掉空壳重现原始报错,加上后消失,
且之后 `Prism.highlightAll` 仍可正常调用。

## 项目地址统一到 code.yangyq.net

原作者署名、原主题地址、README、语言包、JS 里的控制台横幅全部换成自建仓库地址。
LICENSE 文件保持原样不动(MIT 要求保留原始版权声明)。

## CDN 分支改成拿不到就回退本地

原来的 `g_cdn` 分支指向 `cdn.jsdelivr.net/gh/vtrois/kratos`,本仓库在自建
Gitea 上,jsDelivr 只认 github/npm/wordpress,实测该地址 404。现改为仅在
`YANG_THEME_CDN_REPO` 常量存在时才走 CDN,否则一律用
`get_template_directory_uri()`。留一个 404 的地址比留空更危险:页面能开、
样式全丢,极难排查。

## 顺带

- `.gitattributes` 补 `*.mo binary`:原来是靠"含 NUL"启发式判定,不是契约
- `update.json` 的版本号跟进到 3.2.5
- `kratos.min.js` / `kratos.min.css` 用压缩器重新生成,不手工改产物
2026-10-23 12:55:41 +08:00
laoyang a826b26bee 从gitee代码库中初始化项目。 2022-01-07 11:15:42 +08:00