3.2.31 目录「正文内显示」与「浮动显示」拆成两个独立开关(#1)
原来「目录位置」是一个三选一下拉:正文中间 / 页面左侧 / 页面右侧,
等于把「放进正文」和「浮在页边」当成互斥的一件事 —— 想同时在正文
开头放一份、又在边上浮一份,做不到。
改成两个独立勾选,各自带子选项:
· 在正文中显示 → 正文中的对齐(靠左 / 靠右)+ 自动插入位置
· 浮动显示 → 浮动方向(页面左侧 / 页面右侧)
两个都勾就同时出现两份,只勾一个就只出现一种。四种组合都在后台
联动里逐个验证过(探针 70/0)。
新增能力「正文内靠左 / 靠右」是本单的主要新需求。实现上两个要点:
1. 必须给正文目录一个宽度上限(max-width: min(100%, 28em))。
块级盒子默认撑满正文栏,此时 margin:auto 没有剩余空间可分配,
「靠左/靠右」会是一个属性正确、画面纹丝不动的空操作。
2. 用 data-align 属性把方向带下去,CSS 只需两条规则,
不随方向数量膨胀。属性在 yang_toc_slot_html() 里输出。
语义收窄:手写 [TOC] 以前会让整份自动处理全停(连带 rail=false),
现在只手管「正文那一份」—— 手写位置优先,浮动仍由 float 开关独立
决定。这样「精确定位正文位置」和「全站边上都有目录」可以并存。
顺手修掉一处默认值错配:checkbox 用 !empty() 判断会把「键不存在」
和「键明确为空」混成一件事,导致新装站点后台框显示勾上、页面却
不出目录。改用 array_key_exists() 区分的三态判断。
老配置不迁移:3.2.30 的 toc_place 直接作废,不做兼容。
默认 inline=1 / float=0,升级后页面与升级前一致。
重构:把内容过滤器拆成 yang_toc_apply()(纯执行层)+ 运行时壳,
使设置组合能在单进程单测里逐个喂进去(PHP 无法重置函数 static)。
验证
逻辑 101/0 · 逻辑反向 15/15
旧回归 53/0
静态契约 58/0 · 反向 11/11
后台几何+交互 70/0 · 反向 11/11
侧栏几何 73/0 · 反向 8/8
正文对齐几何 23/0 · 反向 5/5(含阴性对照)
包内容契约 59/0 · 反向 9/9
正文对齐的几何探针量到真实的 Δleft = 348px(靠右比靠左),
而不是只断言 data-align 字符串存在。
This commit is contained in:
+129
-33
@@ -21,17 +21,26 @@
|
||||
* 手写标记里写了的参数优先于后台设置,没写的才用后台设置。
|
||||
*
|
||||
* ② 自动:后台「主题设置 → 自动目录」里选好,正文不用写任何东西。
|
||||
* · 目录位置:正文中间 / 页面左侧(浮动)/ 页面右侧(浮动)
|
||||
* · 添加方式:手动 / 自动
|
||||
* · 自动插入位置(只对「正文中间」有效):文章开头 / 第一个 H1、H2 之前 / 文章结尾
|
||||
* · 默认状态:默认显示 / 默认隐藏
|
||||
* · 另外可配目录标题、收录层级、是否显示序号
|
||||
* 显示方式是**两个独立开关**,可同时开启:
|
||||
* · 在正文中显示 → 子选项:正文中的对齐(靠左 / 靠右)、插入位置
|
||||
* (文章开头 / 第一个 H1、H2 之前 / 文章结尾)
|
||||
* · 浮动显示 → 子选项:浮动方向(左侧 / 右侧)
|
||||
* 全开时同一篇文章里正文一份 + 浮动一份,两份指向同一批锚点。
|
||||
* 另外还有:添加方式(手动 / 自动)、默认状态(显示 / 隐藏)、
|
||||
* 目录标题、收录层级、是否显示序号。
|
||||
*
|
||||
* ── 手写永远优先 ──────────────────────────────────────────────
|
||||
* 只要正文里有 [TOC],就**只**按作者写的位置渲染,不再自动插第二份、
|
||||
* 也不出侧栏。理由:作者明确表过态的地方,机器不该再替他做一次决定;
|
||||
* ⚠️ 「靠左 / 靠右」是**正文容器内部**的对齐,不是浮到页面边上;
|
||||
* 「浮动方向」才是贴页面哪一边。这两件事曾经被合并成一个三选一的
|
||||
* place 字段(center/left/right),导致两者永远互斥、无法同时开启。
|
||||
*
|
||||
* ── 手写永远优先(仅限正文那一份)────────────────────────────
|
||||
* 只要正文里有 [TOC],就**只**按作者写的位置渲染正文那一份,不再自动
|
||||
* 插第二份。理由:作者明确表过态的地方,机器不该再替他做一次决定;
|
||||
* 否则老文章升级后会突然多出一个目录,位置还未必是他要的。
|
||||
*
|
||||
* 但浮动面板是**独立开关**:它不属于正文流,作者写了 [TOC] 也没说过
|
||||
* 「不要浮动目录」。所以 float=1 时照样出浮动面板。
|
||||
*
|
||||
* ── 为什么默认收 h1(曾经错设成 "2,3")──────────────────────
|
||||
* 原先的理由是「h1 是文章标题本身,不该进目录」。这个理由是**假的**:
|
||||
* 文章标题输出在 single.php 的 <h1 class="title">,在 the_content
|
||||
@@ -89,9 +98,23 @@ if (!defined('ABSPATH')) {
|
||||
function yang_toc_defaults()
|
||||
{
|
||||
return array(
|
||||
'place' => 'center', // center | left | right
|
||||
/* 两个**互相独立**的显示开关,可同时为 1。
|
||||
*
|
||||
* 为什么不是「三选一」:曾经用单个 place=center/left/right 表达,
|
||||
* 把「放在正文里」和「浮在页面边上」混成了同一个维度的三个值,
|
||||
* 于是两者永远互斥 —— 用户想「正文里放一份、边上再浮一份」时无法表达。
|
||||
* 现在拆成 inline / float 两个开关 + 各自的子选项:
|
||||
* inline 在正文流里插一份
|
||||
* inline_align inline 生效时的对齐:left | right
|
||||
* float 在页面边缘浮一份
|
||||
* float_side float 生效时浮哪边:left | right
|
||||
*/
|
||||
'inline' => 1, // 0 | 1
|
||||
'inline_align' => 'left', // left | right(正文容器内部对齐)
|
||||
'float' => 0, // 0 | 1
|
||||
'float_side' => 'left', // left | right(贴页面哪一边)
|
||||
'mode' => 'manual', // manual | auto
|
||||
'anchor' => 'first', // start | first | end(只对 center 有效)
|
||||
'anchor' => 'first', // start | first | end(只对 inline 有效)
|
||||
'default' => 'show', // show | hide
|
||||
'title' => __('目录', 'kratos'),
|
||||
'levels' => array('1' => 1, '2' => 1, '3' => 1, '4' => 0),
|
||||
@@ -175,7 +198,25 @@ function yang_toc_normalize_settings($raw)
|
||||
};
|
||||
|
||||
$s = array();
|
||||
$s['place'] = $pick('place', array('center', 'left', 'right'));
|
||||
|
||||
/* checkbox 的存值有两种:框架写回时是 '1' 或 ''(也可能 true/false)。
|
||||
* 关键区别在「键不存在」和「键存在但为空」:
|
||||
* · 键不存在 = 从没配过(新站 / 老库升级)= 用默认值
|
||||
* · 键存在且为空 = 用户明确取消勾选 = 0
|
||||
* 用 !empty() 一把梭会把这两种情况混成一件事,结果**新装站点默认不显示
|
||||
* 正文目录** —— 后台勾选框看起来是勾上的(那是 std 预填),页面却不出。
|
||||
* 这种错配极难发现,因为两边单独看都"正常"。 */
|
||||
$flag = function ($key) use ($raw, $d) {
|
||||
if (!array_key_exists($key, $raw)) {
|
||||
return $d[$key]; // 从没配过
|
||||
}
|
||||
return empty($raw[$key]) ? 0 : 1;
|
||||
};
|
||||
|
||||
$s['inline'] = $flag('inline');
|
||||
$s['float'] = $flag('float');
|
||||
$s['inline_align'] = $pick('inline_align', array('left', 'right'));
|
||||
$s['float_side'] = $pick('float_side', array('left', 'right'));
|
||||
$s['mode'] = $pick('mode', array('manual', 'auto'));
|
||||
$s['anchor'] = $pick('anchor', array('start', 'first', 'end'));
|
||||
$s['default'] = $pick('default', array('show', 'hide'));
|
||||
@@ -207,7 +248,8 @@ function yang_toc_settings()
|
||||
}
|
||||
|
||||
$raw = array();
|
||||
foreach (array('place', 'mode', 'anchor', 'default', 'title', 'levels', 'numbering') as $k) {
|
||||
foreach (array('inline', 'inline_align', 'float', 'float_side',
|
||||
'mode', 'anchor', 'default', 'title', 'levels', 'numbering') as $k) {
|
||||
$raw[$k] = kratos_option('toc_' . $k, null);
|
||||
}
|
||||
// 库里没有的键用 null 占位,normalize 时按「未配置」回落默认值
|
||||
@@ -275,15 +317,20 @@ function yang_toc_has_marker($content)
|
||||
* 'start' 文章开头
|
||||
* 'first' 第一个 H1/H2 之前
|
||||
* 'end' 文章结尾
|
||||
* inline_align 'left'/'right' 正文章节的对齐方向
|
||||
* rail bool 是否输出侧边浮动目录
|
||||
* side 'left'/'right' 侧栏挂哪边
|
||||
* settings array 归一化设置
|
||||
*
|
||||
* 规则(顺序即优先级):
|
||||
* ① 正文有 [TOC] ⇒ 只在标记处渲染,不出侧栏、不自动插;
|
||||
* ② 添加方式 = 手动 ⇒ 什么都不自动做;
|
||||
* ③ 目录位置 = 中间 ⇒ 按「自动插入位置」插进正文;
|
||||
* ④ 目录位置 = 左/右 ⇒ 出侧栏,正文不动(插入位置对侧栏无效)。
|
||||
* ① 正文有 [TOC] ⇒ 只在标记处渲染正文那一份;浮动仍按 float 开关独立决定。
|
||||
* 手写标记只管「正文那一份在哪」,没道理连带把浮动面板也关掉 ——
|
||||
* 那是两件事。但正文绝不会出现第二份。
|
||||
* ② 添加方式 = 手动 ⇒ 不自动插正文那份;浮动同样独立决定。
|
||||
* ③ inline=1 ⇒ 按 anchor 插进正文;inline=0 ⇒ 正文不动。
|
||||
* ④ float=1 ⇒ 出浮动面板,方向按 float_side。
|
||||
*
|
||||
* ③ 和 ④ 完全独立,可以同时成立:正文一份 + 浮动一份,共用同一批锚点。
|
||||
*
|
||||
* @param bool $has_marker
|
||||
* @param array $s 设置(不传则取当前设置)
|
||||
@@ -296,12 +343,20 @@ function yang_toc_plan($has_marker = false, $s = null)
|
||||
}
|
||||
|
||||
$plan = array(
|
||||
'inline_at' => null,
|
||||
'rail' => false,
|
||||
'side' => 'left',
|
||||
'settings' => $s,
|
||||
'inline_at' => null,
|
||||
'inline_align' => $s['inline_align'],
|
||||
'rail' => false,
|
||||
'side' => $s['float_side'],
|
||||
'settings' => $s,
|
||||
);
|
||||
|
||||
/* 浮动独立于手写标记与添加方式:它不属于正文流,
|
||||
作者写了 [TOC] 也没说过「不要浮动目录」。 */
|
||||
if (!empty($s['float'])) {
|
||||
$plan['rail'] = true;
|
||||
}
|
||||
|
||||
/* 正文那一份:手写标记 > 自动插入 */
|
||||
if ($has_marker) {
|
||||
$plan['inline_at'] = 'marker';
|
||||
return $plan;
|
||||
@@ -311,11 +366,8 @@ function yang_toc_plan($has_marker = false, $s = null)
|
||||
return $plan;
|
||||
}
|
||||
|
||||
if ($s['place'] === 'center') {
|
||||
if (!empty($s['inline'])) {
|
||||
$plan['inline_at'] = $s['anchor'];
|
||||
} else {
|
||||
$plan['rail'] = true;
|
||||
$plan['side'] = ($s['place'] === 'right') ? 'right' : 'left';
|
||||
}
|
||||
|
||||
return $plan;
|
||||
@@ -769,6 +821,27 @@ function yang_toc_filter_content($content)
|
||||
yang_toc_rail_ready(true);
|
||||
}
|
||||
|
||||
return yang_toc_apply($content, $out, $items, $has_marker, $plan);
|
||||
}
|
||||
add_filter('the_content', 'yang_toc_filter_content', 9);
|
||||
|
||||
/**
|
||||
* 按 plan 把目录落到已补好 id 的正文上(纯函数)
|
||||
*
|
||||
* 单独拆出来是为了让测试能「喂一份设置 → 直接看输出」:上面那个壳依赖
|
||||
* WordPress 运行时(is_singular / in_the_loop / get_the_ID 全要对上),
|
||||
* 而设置是从库里读的、一次请求只能有一份。拆开之后,几十种组合可以在
|
||||
* 同一个进程里逐个跑,不必为了测试给读设置那层开后门。
|
||||
*
|
||||
* @param string $content 原始正文(替换失败时原样返回它)
|
||||
* @param string $out 已补好标题 id 的正文
|
||||
* @param array $items yang_toc_scan() 得到的条目
|
||||
* @param bool $has_marker 原文里有没有手写 [TOC]
|
||||
* @param array $plan yang_toc_plan() 的结果
|
||||
* @return string
|
||||
*/
|
||||
function yang_toc_apply($content, $out, $items, $has_marker, $plan)
|
||||
{
|
||||
/* ── ① 手写标记:按作者写的位置渲染,且不再自动插第二份 ── */
|
||||
if ($has_marker) {
|
||||
/* 后台设置当默认值,标记里写了的属性覆盖它。
|
||||
@@ -786,12 +859,12 @@ function yang_toc_filter_content($content)
|
||||
return is_string($replaced) ? $replaced : $content;
|
||||
}
|
||||
|
||||
/* ── ② 侧栏模式:正文只补 id,目录由 wp_footer 输出 ── */
|
||||
/* ── ② 只出浮动:正文只补 id,目录由 wp_footer 输出 ── */
|
||||
if ($plan['inline_at'] === null) {
|
||||
return $out;
|
||||
}
|
||||
|
||||
/* ── ③ 自动插入 ── */
|
||||
/* ── ③ 正文里插一份 ── */
|
||||
$html = yang_toc_render($items, yang_toc_atts_from_settings($plan['settings']));
|
||||
if ($html === '') {
|
||||
// 一个符合层级的标题都没有 —— 不输出空壳
|
||||
@@ -799,11 +872,36 @@ function yang_toc_filter_content($content)
|
||||
}
|
||||
|
||||
$pos = $plan['inline_at'];
|
||||
$slot = '<div class="yang-toc-slot yang-toc-slot-' . esc_attr($pos) . '">' . $html . '</div>';
|
||||
$slot = yang_toc_slot_html($html, $pos, $plan['inline_align']);
|
||||
|
||||
return yang_toc_inject($out, $slot, $pos);
|
||||
}
|
||||
add_filter('the_content', 'yang_toc_filter_content', 9);
|
||||
|
||||
/**
|
||||
* 正文目录的外层盒子(带插入位置与对齐方向)
|
||||
*
|
||||
* 对齐为什么放在这一层、而不是 .yang-toc 上:
|
||||
* · .yang-toc 自己已经有 24px 上下的外边距,再叠一层 margin 会叠加;
|
||||
* · 靠左/靠右本质是「盒子在正文容器里怎么摆」,属于外层容器的职责;
|
||||
* · 用 data 属性把方向带下去,CSS 只需一条 [data-align="right"] 规则,
|
||||
* 不必再生成 -right 这种 class 变体(class 会越想越多)。
|
||||
*
|
||||
* 注意靠左/靠右**不改变盒子的宽度**:目录块是否撑满正文栏由 .yang-toc
|
||||
* 的样式决定(默认 auto,即撑满)。对齐只在它不撑满时才有可见效果,
|
||||
* 所以这里同时给了 inline-size 相关的钩子,样式里再按需收窄。
|
||||
*
|
||||
* @param string $html 目录 HTML
|
||||
* @param string $pos start | first | end
|
||||
* @param string $align left | right
|
||||
* @return string
|
||||
*/
|
||||
function yang_toc_slot_html($html, $pos, $align = 'left')
|
||||
{
|
||||
$align = ($align === 'right') ? 'right' : 'left';
|
||||
|
||||
return '<div class="yang-toc-slot yang-toc-slot-' . esc_attr($pos) . '"'
|
||||
. ' data-align="' . esc_attr($align) . '">' . $html . '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 把目录插进正文(纯函数)
|
||||
@@ -923,12 +1021,10 @@ function yang_toc_render_rail()
|
||||
if (!$post || !isset($post->post_content)) {
|
||||
return;
|
||||
}
|
||||
// 作者写了 [TOC]:手写优先,不出侧栏
|
||||
if (yang_toc_has_marker($post->post_content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$plan = yang_toc_plan(false);
|
||||
/* 手写 [TOC] 只管「正文那一份在哪」,不再连带关掉浮动面板。
|
||||
两者现在是独立开关,作者写了标记也不等于说过「不要浮动目录」。 */
|
||||
$plan = yang_toc_plan(yang_toc_has_marker($post->post_content));
|
||||
if (!$plan['rail']) {
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user