3.2.30: 文章目录进后台(新增「自动目录」标签页)—— 位置/插入点/默认展开可配,手写 [TOC] 永远优先
后台「主题设置」新增第 5 个标签页「自动目录」,7 个选项: 目录位置 正文中间 / 页面左侧(浮动)/ 页面右侧(浮动) 添加方式 手动(只认 [TOC])/ 自动 自动插入位置 文章开头 / 第一个 H1、H2 之前 / 文章结尾 默认状态 默认显示 / 默认隐藏 另外:目录标题、收录层级(H1~H4)、是否显示序号 设计要点: - 插入方式拆成 toc_mode(manual/auto) x toc_anchor(start/first/end) 两个正交 字段,不合成一个四值下拉:侧栏模式下插入位置毫无意义,合并会产生歧义。 - 手写 [TOC] 绝对优先:yang_toc_plan() 里 $has_marker 最先返回,inline_at ='marker' 且 rail=false —— 否则同一篇会出现两份目录,老文章升级后突然多 出一个目录。这条是刻意守住的。 - 纯函数 + 薄壳分层:normalize_settings / plan / inject / build_list 全部不查库, 单进程可喂几十种组合;settings() 只负责读库 + 调纯函数 + static 缓存。 - <details open> 是唯一可靠的「默认收起」手段:展开态不接受 CSS 覆盖,只能 由 open 属性决定,所以隐藏时必须不输出该属性。 - 侧栏收起态必须 translateX(±100%) 完全移出视口:只做 opacity 会让透明面板 留在原位吃掉正文点击(探针 elementFromPoint 才抓得到)。 - yang_toc_rail_ready() 开关:侧栏在 wp_footer 输出,那时正文可能没走 the_content(缓存插件/OEmbed),标题没 id ⇒ 侧栏链接全跳不动却看起来正常。 宁可不出侧栏。 - 后台联动隐藏「插入位置」不用 jQuery show()/hide():会把 .section 的行内 display 写成 block,悄悄回退 3.2.28 的 display:flow-root BFC 修复。改用 class + display:none!important。 验证(全部在本轮重跑): _test_toc.php 53/0 旧 TOC 行为回归 _toc330_logic.php 88/0 逻辑判据 _toc330_falsify 13/13 注入全变红且红对地方 _geo330_rail.js 73/0 侧栏真实几何(左/右 x 展开/收起 x 宽窄屏) _geo330_falsify 8/8 _guard330.php 46/0 后台控件与运行时默认值双向一致 _guard330_falsify 8/8 _admin330_tab_check 36/0 新标签页几何 + 联动 + 载入态 _admin330_falsify 8/8 _admin328_check 13/0 3.2.28 布局回归 发布:只发 zip + readme + update.json 到 theme-update/,不动服务器主题目录, 用户在 WP 后台自行升级(沿用 3.2.24 起的策略)。
This commit is contained in:
@@ -492,6 +492,16 @@
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
/* 条件字段(如「自动插入位置」只在目录位置=正文中间时才有意义)
|
||||
*
|
||||
* 用 !important 的 display:none 而不是 jQuery 的 show()/hide(),原因见
|
||||
* js/options-custom.js 里 yq_toggle_toc_anchor():.section 的 display 是
|
||||
* flow-root,jQuery 会把行内 display 写成 block,等于回退 3.2.28 的修复。
|
||||
* 这里只隐藏,不改变其它布局属性。 */
|
||||
#optionsframework .section.yq-cond-hide {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 3. 提交栏:与分组标题同一套留白 */
|
||||
#optionsframework-submit {
|
||||
padding: 12px 18px;
|
||||
|
||||
@@ -100,6 +100,22 @@ jQuery(document).ready(function ($) {
|
||||
jQuery('#section-top_describe').show()
|
||||
}
|
||||
|
||||
// 自动目录:只有「目录位置 = 正文中间」时,插入位置才有意义。
|
||||
// 侧栏模式(左/右)下它完全无效 —— 若还留着这个下拉框,用户会以为
|
||||
// 「选了文章结尾却没插进正文」是个 bug。
|
||||
//
|
||||
// 用 class 切换而不是 fadeIn/fadeOut:.section 的 display 是 flow-root
|
||||
// (3.2.28 靠它建独立 BFC,避免被左侧浮动标签列的 clear:both 拉长)。
|
||||
// jQuery 的 show()/hide() 会写行内 display,把那套布局打回 block,
|
||||
// 等于悄悄回退 3.2.28 的修复。
|
||||
function yq_toggle_toc_anchor () {
|
||||
var center = jQuery('#toc_place').val() === 'center'
|
||||
jQuery('#section-toc_anchor').toggleClass('yq-cond-hide', !center)
|
||||
}
|
||||
|
||||
jQuery('#toc_place').on('change', yq_toggle_toc_anchor)
|
||||
yq_toggle_toc_anchor()
|
||||
|
||||
// Loads the color pickers
|
||||
$('.of-color').wpColorPicker()
|
||||
|
||||
|
||||
+102
-6
@@ -381,7 +381,103 @@ function kratos_options()
|
||||
);
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
* 五、站长信息 —— 文章页页脚那张名片
|
||||
* 五、自动目录 —— 把长文切成能跳的章节
|
||||
*
|
||||
* 与「文章配置」分开成独立标签页,是刻意的:目录的选项有六项,
|
||||
* 塞进文章配置里会把「页面布局 / 打赏」这些一眼要看到的东西挤下去。
|
||||
*
|
||||
* ⚠️ 这里的 std 必须与 inc/theme-toc.php 的 yang_toc_defaults() 一致。
|
||||
* 前者只服务「后台预填」和「恢复默认」,运行时一律走
|
||||
* kratos_option($id, 默认值) —— 两处不一致会出现
|
||||
* 「页面照 A 渲染、后台框里显示 B」的错位,而且极难发现。
|
||||
* ══════════════════════════════════════════════════════════ */
|
||||
$options[] = array(
|
||||
'name' => __('自动目录', 'kratos'),
|
||||
'type' => 'heading',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('目录位置', 'kratos'),
|
||||
'desc' => __('<b>正文中间</b>:目录是正文的一部分,按下面的「自动插入位置」插进去。<br><b>页面左侧 / 右侧</b>:目录变成贴在页面边缘的浮动面板,点边上的小标签滑出,正文里不放任何东西(「自动插入位置」对它无效)。窄屏(手机)上也是从边缘滑出的抽屉,不会挤占正文。', 'kratos'),
|
||||
'id' => 'toc_place',
|
||||
'std' => 'center',
|
||||
'type' => 'select',
|
||||
'options' => array(
|
||||
'center' => __('正文中间', 'kratos'),
|
||||
'left' => __('页面左侧(浮动)', 'kratos'),
|
||||
'right' => __('页面右侧(浮动)', 'kratos'),
|
||||
),
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('添加方式', 'kratos'),
|
||||
'desc' => __('<b>手动</b>:不自动加,只有正文里手写了 <b>[TOC]</b> 的地方才显示(老文章的行为,升级后不会突然多出目录)。<br><b>自动</b>:由主题自己加,不用改任何文章。', 'kratos'),
|
||||
'id' => 'toc_mode',
|
||||
'std' => 'manual',
|
||||
'type' => 'select',
|
||||
'options' => array(
|
||||
'manual' => __('手动(只在写 [TOC] 的地方显示)', 'kratos'),
|
||||
'auto' => __('自动添加', 'kratos'),
|
||||
),
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('自动插入位置', 'kratos'),
|
||||
'desc' => __('只在「添加方式 = 自动」且「目录位置 = 正文中间」时起作用。<br><b>第一个标题之前</b>取正文里最先出现的 h1 或 h2 —— 文章大标题在正文之外,不会被算进去;一个 h1/h2 都没有时退回插在文章开头。', 'kratos'),
|
||||
'id' => 'toc_anchor',
|
||||
'std' => 'first',
|
||||
'type' => 'select',
|
||||
'options' => array(
|
||||
'start' => __('文章开头', 'kratos'),
|
||||
'first' => __('第一个标题之前(H1 / H2)', 'kratos'),
|
||||
'end' => __('文章结尾', 'kratos'),
|
||||
),
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('默认状态', 'kratos'),
|
||||
'desc' => __('<b>默认显示</b>:进来就能看到全部条目。<b>默认隐藏</b>:先只露出标题栏(或侧边的标签),读者自己点开。<b>较长或标题很多的文章建议默认隐藏</b>,否则目录会占掉一屏。<br>注:这里只管「默认长什么样」,读者点开后仍然可以正常使用;正文里手写的 <b>[TOC]</b> 也可以用 <b>open="0"</b> 单独指定。', 'kratos'),
|
||||
'id' => 'toc_default',
|
||||
'std' => 'show',
|
||||
'type' => 'select',
|
||||
'options' => array(
|
||||
'show' => __('默认显示', 'kratos'),
|
||||
'hide' => __('默认隐藏', 'kratos'),
|
||||
),
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('目录标题', 'kratos'),
|
||||
'desc' => __('显示在目录顶部的那行字。留空则用「目录」。', 'kratos'),
|
||||
'id' => 'toc_title',
|
||||
'std' => __('目录', 'kratos'),
|
||||
'type' => 'text',
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('收录标题级别', 'kratos'),
|
||||
'desc' => __('勾选要进目录的标题级别。<b>正文里的 h1 是作者自己写的章节标题</b>(文章大标题在正文之外,不会被收进来),所以要用 h1 分章的文章也建议勾上 h1。<br>h4 以下通常是细节,勾上会让目录变得很长。', 'kratos'),
|
||||
'id' => 'toc_levels',
|
||||
'std' => array('1' => 1, '2' => 1, '3' => 1, '4' => 0),
|
||||
'type' => 'multicheck',
|
||||
'options' => array(
|
||||
'1' => array('name' => 'H1', 'note' => __('一级标题', 'kratos')),
|
||||
'2' => array('name' => 'H2', 'note' => __('二级标题', 'kratos')),
|
||||
'3' => array('name' => 'H3', 'note' => __('三级标题', 'kratos')),
|
||||
'4' => array('name' => 'H4', 'note' => __('四级标题', 'kratos')),
|
||||
),
|
||||
);
|
||||
|
||||
$options[] = array(
|
||||
'name' => __('显示序号', 'kratos'),
|
||||
'desc' => __('在条目前面加 1 / 1.1 / 1.1.1 这样的序号。', 'kratos'),
|
||||
'id' => 'toc_numbering',
|
||||
'std' => '0',
|
||||
'type' => 'checkbox',
|
||||
);
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
* 六、站长信息 —— 文章页页脚那张名片
|
||||
* ══════════════════════════════════════════════════════════ */
|
||||
$options[] = array(
|
||||
'name' => __('站长信息', 'kratos'),
|
||||
@@ -411,7 +507,7 @@ function kratos_options()
|
||||
);
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
* 六、页脚配置 —— 页面最底下那几行
|
||||
* 七、页脚配置 —— 页面最底下那几行
|
||||
* ══════════════════════════════════════════════════════════ */
|
||||
$options[] = array(
|
||||
'name' => __('页脚配置', 'kratos'),
|
||||
@@ -465,7 +561,7 @@ function kratos_options()
|
||||
);
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
* 七、哀悼模式 —— 整站级功能,与首页无关
|
||||
* 八、哀悼模式 —— 整站级功能,与首页无关
|
||||
*
|
||||
* 这一组原来错放在「首页配置」下面。它影响的是整站配色
|
||||
* (首页、文章页、说明页都变黑白),放首页下面会让人以为
|
||||
@@ -560,7 +656,7 @@ function kratos_options()
|
||||
);
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
* 八、收录与统计 —— 给搜索引擎看的
|
||||
* 九、收录与统计 —— 给搜索引擎看的
|
||||
* ══════════════════════════════════════════════════════════ */
|
||||
$options[] = array(
|
||||
'name' => __('收录与统计', 'kratos'),
|
||||
@@ -618,7 +714,7 @@ function kratos_options()
|
||||
);
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
* 九、邮件通知 —— SMTP 与评论提醒
|
||||
* 十、邮件通知 —— SMTP 与评论提醒
|
||||
* ══════════════════════════════════════════════════════════ */
|
||||
$options[] = array(
|
||||
'name' => __('邮件通知', 'kratos'),
|
||||
@@ -716,7 +812,7 @@ function kratos_options()
|
||||
);
|
||||
|
||||
/* ══════════════════════════════════════════════════════════
|
||||
* 十、自定义代码 —— 兜底用,放最后
|
||||
* 十一、自定义代码 —— 兜底用,放最后
|
||||
*
|
||||
* 3.2.29 起这两项默认都为空:站点自己的通用样式
|
||||
* (段落缩进、h2 蓝条、列表缩进)已并入 assets/css/site.css,
|
||||
|
||||
+598
-81
@@ -2,21 +2,35 @@
|
||||
/**
|
||||
* 文章目录(TOC)
|
||||
*
|
||||
* ── 用法 ──────────────────────────────────────────────────────
|
||||
* 在文章内容的任意位置写一行:
|
||||
* ── 两种用法 ──────────────────────────────────────────────────
|
||||
* ① 手动:在文章内容的任意位置写一行
|
||||
*
|
||||
* [TOC]
|
||||
* [TOC]
|
||||
*
|
||||
* 这个位置就会出现本文的目录,点条目跳到对应标题。
|
||||
* 这个位置就会出现本文的目录,点条目跳到对应标题。
|
||||
* 可调参数(都可省略):
|
||||
*
|
||||
* 可调参数(都可省略):
|
||||
* [TOC title="本文目录" levels="1,2,3" numbering="1" toggle="0" open="0"]
|
||||
*
|
||||
* [TOC title="本文目录" levels="1,2,3" numbering="1" toggle="0"]
|
||||
* · title 目录标题,默认「目录」
|
||||
* · levels 收录哪几级标题,逗号分隔,默认 "1,2,3"
|
||||
* · numbering 1 = 条目前加 1 / 1.1 这样的序号,默认 0
|
||||
* · toggle 1 = 标题可点击折叠(<details>),默认 1
|
||||
* · open 1 = 默认展开,0 = 默认收起,默认 1
|
||||
*
|
||||
* · title 目录标题,默认「目录」
|
||||
* · levels 收录哪几级标题,逗号分隔,默认 "1,2,3"
|
||||
* · numbering 1 = 条目前加 1 / 1.1 这样的序号,默认 0
|
||||
* · toggle 1 = 标题可点击折叠,默认 1
|
||||
* 手写标记里写了的参数优先于后台设置,没写的才用后台设置。
|
||||
*
|
||||
* ② 自动:后台「主题设置 → 自动目录」里选好,正文不用写任何东西。
|
||||
* · 目录位置:正文中间 / 页面左侧(浮动)/ 页面右侧(浮动)
|
||||
* · 添加方式:手动 / 自动
|
||||
* · 自动插入位置(只对「正文中间」有效):文章开头 / 第一个 H1、H2 之前 / 文章结尾
|
||||
* · 默认状态:默认显示 / 默认隐藏
|
||||
* · 另外可配目录标题、收录层级、是否显示序号
|
||||
*
|
||||
* ── 手写永远优先 ──────────────────────────────────────────────
|
||||
* 只要正文里有 [TOC],就**只**按作者写的位置渲染,不再自动插第二份、
|
||||
* 也不出侧栏。理由:作者明确表过态的地方,机器不该再替他做一次决定;
|
||||
* 否则老文章升级后会突然多出一个目录,位置还未必是他要的。
|
||||
*
|
||||
* ── 为什么默认收 h1(曾经错设成 "2,3")──────────────────────
|
||||
* 原先的理由是「h1 是文章标题本身,不该进目录」。这个理由是**假的**:
|
||||
@@ -27,7 +41,7 @@
|
||||
* 一旦正文用 h1 划分章节(很常见:从 Markdown 或别的编辑器粘过来,
|
||||
* 或原本的 ## 被降级),默认 "2,3" 会把这些一级章节整段丢掉,
|
||||
* 于是二级标题集体失去父级、目录变成平铺 —— 层级信息全错。
|
||||
* 所以默认跟 h2/h3 一起收 h1,最多到 h3(h4 以下通常是细节,不进目录)。
|
||||
* 所以默认跟 h2/h3 一起收 h1,最多到 h4(h5 以下通常是细节,不进目录)。
|
||||
*
|
||||
* ── 为什么在 the_content 优先级 9 处理,而不是只靠短代码 ──────
|
||||
* 目录能点,前提是「标题有 id」且「目录链接指向那个 id」。这两件事
|
||||
@@ -40,7 +54,7 @@
|
||||
*
|
||||
* 所以在优先级 9(早于 wpautop 和 do_shortcode)一次做完:
|
||||
* ① 给每个 h1~h6 补 id(已有 id 的沿用,不动别人的锚点)
|
||||
* ② 把 [TOC] 直接替换成目录 HTML
|
||||
* ② 把 [TOC] 直接替换成目录 HTML,或按后台设置插到指定位置
|
||||
*
|
||||
* 放在 <div> 里是刻意的:wpautop 会把「块级元素」两边的 <p> 剥掉,
|
||||
* 所以不会出现 <p><div>…</div></p> 这种非法嵌套。
|
||||
@@ -49,6 +63,12 @@
|
||||
* 调用 [TOC](小工具、页面构建器),短代码那条路也能用,且复用
|
||||
* 同一个渲染函数,不会出现两套行为。
|
||||
*
|
||||
* ── 侧栏目录为什么在 wp_footer 输出 ──────────────────────────
|
||||
* 侧栏是 position:fixed 的独立元素,不属于正文流。挂在正文里会让
|
||||
* wpautop、各种内容过滤器和「正文首块」样式都去处理一个本不属于
|
||||
* 正文的盒子,而且一旦有人调 the_content(相关文章、摘要),就会
|
||||
* 跟着重复输出。放在页面末尾、只认「当前请求就是这篇文章」,最稳。
|
||||
*
|
||||
* @author Yongquan Yang <yangyq.net>
|
||||
* @license MIT License
|
||||
*/
|
||||
@@ -57,6 +77,250 @@ if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 后台选项的运行时默认值(唯一事实来源)
|
||||
*
|
||||
* ⚠️ 只改这里不够:后台表单的 std(inc/theme-options.php)是给
|
||||
* 「后台预填」和「恢复默认」用的,两者必须一致,否则会出现
|
||||
* 「页面照 A 渲染、后台框里显示 B」的错位。改默认值时两处一起改。
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function yang_toc_defaults()
|
||||
{
|
||||
return array(
|
||||
'place' => 'center', // center | left | right
|
||||
'mode' => 'manual', // manual | auto
|
||||
'anchor' => 'first', // start | first | end(只对 center 有效)
|
||||
'default' => 'show', // show | hide
|
||||
'title' => __('目录', 'kratos'),
|
||||
'levels' => array('1' => 1, '2' => 1, '3' => 1, '4' => 0),
|
||||
'numbering' => 0,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 把「收录层级」的多种存法归一成 array(1..6 => true)
|
||||
*
|
||||
* 三种输入都可能出现:
|
||||
* · 库里的 multicheck 值:array('1'=>1,'2'=>1,'3'=>1,'4'=>0)
|
||||
* · 短代码属性:"2,3"
|
||||
* · 什么都没配:null / ''
|
||||
*
|
||||
* 全空时不返回空数组,而是回落到 1,2,3 —— 「一个级别都没勾」和
|
||||
* 「目录不显示」在用户心里不是一回事:前者多半是误操作,后者应该
|
||||
* 用「插入位置=手动且不写标记」表达。这里宁可退回可见的默认。
|
||||
*
|
||||
* @param mixed $raw
|
||||
* @return array
|
||||
*/
|
||||
function yang_toc_normalize_levels($raw)
|
||||
{
|
||||
$levels = array();
|
||||
|
||||
if (is_array($raw)) {
|
||||
foreach ($raw as $k => $v) {
|
||||
if (empty($v)) {
|
||||
continue;
|
||||
}
|
||||
$n = (int) $k;
|
||||
if ($n >= 1 && $n <= 6) {
|
||||
$levels[$n] = true;
|
||||
}
|
||||
}
|
||||
} elseif (is_string($raw)) {
|
||||
foreach (preg_split('/[\s,,、]+/u', $raw) as $one) {
|
||||
if ($one === '') {
|
||||
continue;
|
||||
}
|
||||
$n = (int) $one;
|
||||
if ($n >= 1 && $n <= 6) {
|
||||
$levels[$n] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($levels)) {
|
||||
$levels = array(1 => true, 2 => true, 3 => true);
|
||||
}
|
||||
|
||||
return $levels;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把「原始配置」归一化成可用的设置(纯函数,不查库)
|
||||
*
|
||||
* 为什么单独拆出来:这里要判的坏值很多(下拉框被写坏、导入配置带了
|
||||
* 别的值、老版本残留的键),而真正的判断逻辑与 WordPress 无关。
|
||||
* 拆成纯函数之后,测试可以直接喂几十种组合,不用动数据库、
|
||||
* 也不用为了测试给「读设置」那层加后门。
|
||||
*
|
||||
* 非法值一律回落到默认值:后台的下拉框理论上不会送来非法值,但
|
||||
* 配置可能被「恢复默认」「导入配置」「手改数据库」写坏。这里不抛错、
|
||||
* 不报警,只是让页面别因为一个坏字符串变成空白正文。
|
||||
*
|
||||
* @param array $raw 原始键值(可能缺键、可能类型不对)
|
||||
* @return array
|
||||
*/
|
||||
function yang_toc_normalize_settings($raw)
|
||||
{
|
||||
$d = yang_toc_defaults();
|
||||
if (!is_array($raw)) {
|
||||
$raw = array();
|
||||
}
|
||||
|
||||
$pick = function ($key, $allowed) use ($raw, $d) {
|
||||
$v = array_key_exists($key, $raw) ? $raw[$key] : $d[$key];
|
||||
return in_array($v, $allowed, true) ? $v : $d[$key];
|
||||
};
|
||||
|
||||
$s = array();
|
||||
$s['place'] = $pick('place', array('center', 'left', 'right'));
|
||||
$s['mode'] = $pick('mode', array('manual', 'auto'));
|
||||
$s['anchor'] = $pick('anchor', array('start', 'first', 'end'));
|
||||
$s['default'] = $pick('default', array('show', 'hide'));
|
||||
|
||||
$title = array_key_exists('title', $raw) ? $raw['title'] : $d['title'];
|
||||
$title = is_string($title) ? trim(wp_strip_all_tags($title)) : '';
|
||||
$s['title'] = $title !== '' ? $title : $d['title'];
|
||||
|
||||
$s['levels'] = yang_toc_normalize_levels(
|
||||
array_key_exists('levels', $raw) ? $raw['levels'] : $d['levels']
|
||||
);
|
||||
|
||||
// checkbox 存的是 '1' / true / false,统一成 0/1
|
||||
$s['numbering'] = !empty($raw['numbering']) ? 1 : 0;
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前设置(从库里读一次,本请求内缓存)
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
function yang_toc_settings()
|
||||
{
|
||||
static $cache = null;
|
||||
if ($cache !== null) {
|
||||
return $cache;
|
||||
}
|
||||
|
||||
$raw = array();
|
||||
foreach (array('place', 'mode', 'anchor', 'default', 'title', 'levels', 'numbering') as $k) {
|
||||
$raw[$k] = kratos_option('toc_' . $k, null);
|
||||
}
|
||||
// 库里没有的键用 null 占位,normalize 时按「未配置」回落默认值
|
||||
foreach ($raw as $k => $v) {
|
||||
if ($v === null) {
|
||||
unset($raw[$k]);
|
||||
}
|
||||
}
|
||||
|
||||
$cache = yang_toc_normalize_settings($raw);
|
||||
return $cache;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把设置翻译成渲染参数(与短代码属性同名同格式)
|
||||
*
|
||||
* 这样「后台设置」和「手写属性」能直接 array_merge,不需要两套
|
||||
* 参数体系 —— 两套体系是同类功能最经典的 bug 来源:改了后台不生效、
|
||||
* 或者手写参数被后台悄悄覆盖。
|
||||
*
|
||||
* @param array $s 设置(不传则取当前设置)
|
||||
* @return array
|
||||
*/
|
||||
function yang_toc_atts_from_settings($s = null)
|
||||
{
|
||||
if (!is_array($s)) {
|
||||
$s = yang_toc_settings();
|
||||
}
|
||||
|
||||
$levels = array();
|
||||
$map = is_array($s['levels']) ? $s['levels'] : yang_toc_normalize_levels($s['levels']);
|
||||
ksort($map);
|
||||
foreach ($map as $k => $v) {
|
||||
if (!empty($v)) {
|
||||
$levels[] = $k;
|
||||
}
|
||||
}
|
||||
|
||||
return array(
|
||||
'title' => $s['title'],
|
||||
'levels' => implode(',', $levels),
|
||||
'numbering' => !empty($s['numbering']) ? '1' : '0',
|
||||
'toggle' => '1',
|
||||
'open' => $s['default'] === 'show' ? '1' : '0',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 正文里是否有手写的 [TOC] 标记
|
||||
*
|
||||
* @param string $content
|
||||
* @return bool
|
||||
*/
|
||||
function yang_toc_has_marker($content)
|
||||
{
|
||||
return is_string($content) && stripos($content, '[toc') !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 本次请求该怎么办
|
||||
*
|
||||
* 返回:
|
||||
* inline_at null 正文里不插目录
|
||||
* 'marker' 按作者写的 [TOC] 位置插
|
||||
* 'start' 文章开头
|
||||
* 'first' 第一个 H1/H2 之前
|
||||
* 'end' 文章结尾
|
||||
* rail bool 是否输出侧边浮动目录
|
||||
* side 'left'/'right' 侧栏挂哪边
|
||||
* settings array 归一化设置
|
||||
*
|
||||
* 规则(顺序即优先级):
|
||||
* ① 正文有 [TOC] ⇒ 只在标记处渲染,不出侧栏、不自动插;
|
||||
* ② 添加方式 = 手动 ⇒ 什么都不自动做;
|
||||
* ③ 目录位置 = 中间 ⇒ 按「自动插入位置」插进正文;
|
||||
* ④ 目录位置 = 左/右 ⇒ 出侧栏,正文不动(插入位置对侧栏无效)。
|
||||
*
|
||||
* @param bool $has_marker
|
||||
* @param array $s 设置(不传则取当前设置)
|
||||
* @return array
|
||||
*/
|
||||
function yang_toc_plan($has_marker = false, $s = null)
|
||||
{
|
||||
if (!is_array($s)) {
|
||||
$s = yang_toc_settings();
|
||||
}
|
||||
|
||||
$plan = array(
|
||||
'inline_at' => null,
|
||||
'rail' => false,
|
||||
'side' => 'left',
|
||||
'settings' => $s,
|
||||
);
|
||||
|
||||
if ($has_marker) {
|
||||
$plan['inline_at'] = 'marker';
|
||||
return $plan;
|
||||
}
|
||||
|
||||
if ($s['mode'] === 'manual') {
|
||||
return $plan;
|
||||
}
|
||||
|
||||
if ($s['place'] === 'center') {
|
||||
$plan['inline_at'] = $s['anchor'];
|
||||
} else {
|
||||
$plan['rail'] = true;
|
||||
$plan['side'] = ($s['place'] === 'right') ? 'right' : 'left';
|
||||
}
|
||||
|
||||
return $plan;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把标题文本变成锚点 id
|
||||
*
|
||||
@@ -192,42 +456,25 @@ function yang_toc_parse_atts($raw)
|
||||
}
|
||||
|
||||
/**
|
||||
* 把条目数组渲染成目录 HTML
|
||||
* 把条目数组渲染成 <ul class="yang-toc-list">(只出列表,不含外壳)
|
||||
*
|
||||
* 侧栏只要列表,内嵌要完整盒子。拆成两个函数是为了让两边共用同一份
|
||||
* 嵌套/序号算法 —— 抄一份出来必然会漂移(改了一边忘了另一边)。
|
||||
*
|
||||
* @param array $items 条目(含 level/text/id)
|
||||
* @param array $atts 解析后的属性
|
||||
* @return string 空串表示不该输出(没有可用标题)
|
||||
* @return array array('list' => HTML, 'picked' => 收录的条目数)
|
||||
*/
|
||||
function yang_toc_render($items, $atts = array())
|
||||
function yang_toc_build_list($items, $atts = array())
|
||||
{
|
||||
if (empty($items)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// ── 参数归一 ──
|
||||
$title = isset($atts['title']) && trim($atts['title']) !== ''
|
||||
? trim($atts['title'])
|
||||
: __('目录', 'kratos');
|
||||
|
||||
// levels:默认 1,2,3。文章标题本身在 the_content 之外,不会被扫到,
|
||||
// 所以正文里的 h1 是作者写的一级章节标题,必须收 —— 详见文件头注释。
|
||||
$levels = array();
|
||||
$raw_levels = isset($atts['levels']) ? (string) $atts['levels'] : '1,2,3';
|
||||
foreach (preg_split('/[\s,,、]+/u', $raw_levels) as $one) {
|
||||
if ($one === '') {
|
||||
continue;
|
||||
}
|
||||
$n = (int) $one;
|
||||
if ($n >= 1 && $n <= 6) {
|
||||
$levels[$n] = true;
|
||||
}
|
||||
}
|
||||
if (empty($levels)) {
|
||||
$levels = array(1 => true, 2 => true, 3 => true);
|
||||
return array('list' => '', 'picked' => 0);
|
||||
}
|
||||
|
||||
$levels = yang_toc_normalize_levels(
|
||||
isset($atts['levels']) ? (string) $atts['levels'] : ''
|
||||
);
|
||||
$numbering = isset($atts['numbering']) && (string) $atts['numbering'] === '1';
|
||||
$toggle = !isset($atts['toggle']) || (string) $atts['toggle'] === '1';
|
||||
|
||||
// ── 过滤出要收录的标题 ──
|
||||
$picked = array();
|
||||
@@ -238,7 +485,7 @@ function yang_toc_render($items, $atts = array())
|
||||
}
|
||||
if (empty($picked)) {
|
||||
// 文章里一个符合条件的标题都没有 —— 用户明确要求「没有就不显示」
|
||||
return '';
|
||||
return array('list' => '', 'picked' => 0);
|
||||
}
|
||||
|
||||
// ── 序号(1 / 1.1 / 1.1.1)──
|
||||
@@ -255,7 +502,6 @@ function yang_toc_render($items, $atts = array())
|
||||
}
|
||||
}
|
||||
$parts = array();
|
||||
$base_found = false;
|
||||
foreach ($counter as $k => $v) {
|
||||
$parts[] = $v;
|
||||
}
|
||||
@@ -301,7 +547,6 @@ function yang_toc_render($items, $atts = array())
|
||||
|
||||
$o = array();
|
||||
$prev_depth = -1;
|
||||
$max_depth = 0;
|
||||
|
||||
foreach ($picked as $i => $it) {
|
||||
$lv = $it['level'];
|
||||
@@ -336,30 +581,53 @@ function yang_toc_render($items, $atts = array())
|
||||
. '</a>';
|
||||
|
||||
$prev_depth = $d;
|
||||
if ($d > $max_depth) {
|
||||
$max_depth = $d;
|
||||
}
|
||||
}
|
||||
|
||||
// 收尾:关最后一条 + 逐层收掉所有 ul
|
||||
if (!empty($picked)) {
|
||||
$o[] = '</li>';
|
||||
for ($k = $prev_depth; $k > 0; $k--) {
|
||||
$o[] = '</ul></li>';
|
||||
}
|
||||
$o[] = '</ul>';
|
||||
$o[] = '</li>';
|
||||
for ($k = $prev_depth; $k > 0; $k--) {
|
||||
$o[] = '</ul></li>';
|
||||
}
|
||||
$o[] = '</ul>';
|
||||
|
||||
$list = implode('', $o);
|
||||
return array('list' => implode('', $o), 'picked' => count($picked));
|
||||
}
|
||||
|
||||
/**
|
||||
* 把条目数组渲染成目录 HTML(完整盒子,用于正文内嵌)
|
||||
*
|
||||
* @param array $items 条目(含 level/text/id)
|
||||
* @param array $atts 解析后的属性
|
||||
* @return string 空串表示不该输出(没有可用标题)
|
||||
*/
|
||||
function yang_toc_render($items, $atts = array())
|
||||
{
|
||||
$built = yang_toc_build_list($items, $atts);
|
||||
if ($built['list'] === '') {
|
||||
return '';
|
||||
}
|
||||
$list = $built['list'];
|
||||
$count = $built['picked'];
|
||||
|
||||
// ── 参数归一 ──
|
||||
$title = isset($atts['title']) && trim($atts['title']) !== ''
|
||||
? trim($atts['title'])
|
||||
: __('目录', 'kratos');
|
||||
|
||||
$toggle = !isset($atts['toggle']) || (string) $atts['toggle'] === '1';
|
||||
$open = !isset($atts['open']) || (string) $atts['open'] === '1';
|
||||
|
||||
/* 折叠用 <details>:原生元素,不需要 JS 就能工作。
|
||||
JS 被禁用 / 加载失败时,目录仍然可以展开收起 ——
|
||||
比用自定义 div + class 切换更抗故障。 */
|
||||
比用自定义 div + class 切换更抗故障。
|
||||
|
||||
默认收起时不写 open 属性(details 的展开态只由 open 决定,
|
||||
不认 CSS)——「默认隐藏」靠的就是这一点。 */
|
||||
if ($toggle) {
|
||||
$html = '<details class="yang-toc" open>'
|
||||
$html = '<details class="yang-toc"' . ($open ? ' open' : '') . '>'
|
||||
. '<summary class="yang-toc-head">'
|
||||
. '<span class="yang-toc-title">' . esc_html($title) . '</span>'
|
||||
. '<span class="yang-toc-count">' . count($picked) . '</span>'
|
||||
. '<span class="yang-toc-count">' . $count . '</span>'
|
||||
. '</summary>'
|
||||
. '<nav class="yang-toc-body" aria-label="' . esc_attr($title) . '">'
|
||||
. $list
|
||||
@@ -369,7 +637,7 @@ function yang_toc_render($items, $atts = array())
|
||||
$html = '<div class="yang-toc">'
|
||||
. '<div class="yang-toc-head">'
|
||||
. '<span class="yang-toc-title">' . esc_html($title) . '</span>'
|
||||
. '<span class="yang-toc-count">' . count($picked) . '</span>'
|
||||
. '<span class="yang-toc-count">' . $count . '</span>'
|
||||
. '</div>'
|
||||
. '<nav class="yang-toc-body" aria-label="' . esc_attr($title) . '">'
|
||||
. $list
|
||||
@@ -380,42 +648,194 @@ function yang_toc_render($items, $atts = array())
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前请求是不是「一篇文章的正文」
|
||||
*
|
||||
* 自动插入和侧栏只在这里生效。散文、列表页、附件页都不该冒出目录。
|
||||
*
|
||||
* 为什么还要求「就是被查询的那一篇」:主题里可能用 related posts 之类的
|
||||
* 二次循环,那些内容也会过 the_content。按 id 比对能确保只有正主
|
||||
* 才拿到目录,不会让「相关文章」的标题也被塞进目录里。
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
function yang_toc_is_article_page()
|
||||
{
|
||||
if (is_admin() || !is_singular() || is_attachment()) {
|
||||
return false;
|
||||
}
|
||||
if (!in_the_loop()) {
|
||||
return false;
|
||||
}
|
||||
$qid = (int) get_queried_object_id();
|
||||
$cid = (int) get_the_ID();
|
||||
return ($qid > 0 && $cid === $qid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 侧栏是否可以输出(正文已被本主题扫过一遍的凭据)
|
||||
*
|
||||
* ── 为什么需要这个开关 ────────────────────────────────────────
|
||||
* 侧栏在 wp_footer 输出,那时正文可能**根本没走** the_content 过滤器:
|
||||
* 模板可能直接用 get_the_content()、OEmbed、缓存插件直出 HTML,
|
||||
* 或者正文被别的插件接管。这些情况下正文里的标题**没有 id**,
|
||||
* 而侧栏照旧按扫描结果生成一堆 href="#xxx" —— 点下去一个都跳不动,
|
||||
* 而且看起来完全正常(目录有内容、链接有颜色),排查极费劲。
|
||||
*
|
||||
* 所以侧栏不自己判断"该不该显示",而是只认「正文过滤器确实为这篇文章
|
||||
* 补过 id」这个事实。宁可不出侧栏,也不出一份点不动的目录。
|
||||
*
|
||||
* @param bool|null $set 传入 true/false 则写入
|
||||
* @return bool
|
||||
*/
|
||||
function yang_toc_rail_ready($set = null)
|
||||
{
|
||||
static $ready = false;
|
||||
if ($set !== null) {
|
||||
$ready = (bool) $set;
|
||||
}
|
||||
return $ready;
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描结果缓存(按文章 id)
|
||||
*
|
||||
* 正文过滤器和侧栏分别在两个时机跑,必须拿到**同一份**条目(id 一旦
|
||||
* 不一致,点侧栏就跳不到标题)。正文过滤器先跑,把结果存这里;
|
||||
* 侧栏直接复用。若侧栏先跑(或正文没走过滤器),退回扫描原文。
|
||||
*
|
||||
* @param int $post_id
|
||||
* @param array $scan 传入则写入缓存
|
||||
* @return array array('content' => ..., 'items' => ...)
|
||||
*/
|
||||
function yang_toc_scan_cache($post_id, $scan = null)
|
||||
{
|
||||
static $cache = array();
|
||||
$post_id = (int) $post_id;
|
||||
|
||||
if (is_array($scan)) {
|
||||
$cache[$post_id] = $scan;
|
||||
return $scan;
|
||||
}
|
||||
if (isset($cache[$post_id])) {
|
||||
return $cache[$post_id];
|
||||
}
|
||||
|
||||
$post = get_post($post_id);
|
||||
$cache[$post_id] = $post && isset($post->post_content)
|
||||
? yang_toc_scan($post->post_content)
|
||||
: array('content' => '', 'items' => array());
|
||||
|
||||
return $cache[$post_id];
|
||||
}
|
||||
|
||||
/**
|
||||
* the_content 主处理
|
||||
*
|
||||
* 顺序很重要:先给标题补 id(扫描出条目),再替换 [TOC]。
|
||||
* 反过来的话,[TOC] 生成时拿不到标题清单。
|
||||
* 顺序很重要:先给标题补 id(扫描出条目),再插目录。
|
||||
* 反过来的话,目录生成时拿不到标题清单。
|
||||
*/
|
||||
function yang_toc_filter_content($content)
|
||||
{
|
||||
if (is_admin() || !is_string($content) || $content === '') {
|
||||
return $content;
|
||||
}
|
||||
if (stripos($content, '[toc') === false) {
|
||||
// 没有 [TOC]:什么都不做。
|
||||
// 不动标题 id 是刻意的 —— 不写 [TOC] 的文章不该被塞进一堆 id,
|
||||
// 那会改变页面上已有的选择器和锚点行为。
|
||||
|
||||
$has_marker = yang_toc_has_marker($content);
|
||||
$plan = yang_toc_plan($has_marker);
|
||||
|
||||
/* 什么都不用做:没有手写标记、也没有自动插入、也不出侧栏。
|
||||
这时**连 id 都不补**是刻意的 —— 没开目录的文章不该被塞进一堆 id,
|
||||
那会改变页面上已有的选择器和锚点行为。 */
|
||||
if ($plan['inline_at'] === null && !$has_marker && !$plan['rail']) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
/* 手写标记之外的一切(自动插入 / 侧栏)只在真正的文章正文里干。
|
||||
手写标记放到哪里都认,那是作者自己的选择。 */
|
||||
$is_article = yang_toc_is_article_page();
|
||||
if ($plan['inline_at'] !== 'marker' && !$is_article) {
|
||||
return $content;
|
||||
}
|
||||
|
||||
$scan = yang_toc_scan($content);
|
||||
$items = $scan['items'];
|
||||
$out = $scan['content'];
|
||||
|
||||
/* 替换 [TOC] / [toc] / [TOC ...]。大小写都认 ——
|
||||
用户不会记得"这里的标签必须大写",凭直觉写 [toc] 就该能用。 */
|
||||
$out = preg_replace_callback(
|
||||
'/\[toc(\s[^\]]*)?\]/i',
|
||||
function ($m) use ($items) {
|
||||
$atts = yang_toc_parse_atts(isset($m[1]) ? $m[1] : '');
|
||||
return yang_toc_render($items, $atts);
|
||||
},
|
||||
$scan['content']
|
||||
);
|
||||
// 侧栏要用的那份条目必须与正文里的 id 完全一致,存起来给 footer 用
|
||||
if ($is_article) {
|
||||
yang_toc_scan_cache(get_the_ID(), $scan);
|
||||
// 正文确实被扫过、标题确实补了 id ⇒ 侧栏的链接才保证跳得动
|
||||
yang_toc_rail_ready(true);
|
||||
}
|
||||
|
||||
return is_string($out) ? $out : $content;
|
||||
/* ── ① 手写标记:按作者写的位置渲染,且不再自动插第二份 ── */
|
||||
if ($has_marker) {
|
||||
/* 后台设置当默认值,标记里写了的属性覆盖它。
|
||||
替换 [TOC] / [toc] / [TOC ...]。大小写都认 ——
|
||||
用户不会记得"这里的标签必须大写",凭直觉写 [toc] 就该能用。 */
|
||||
$base = yang_toc_atts_from_settings($plan['settings']);
|
||||
$replaced = preg_replace_callback(
|
||||
'/\[toc(\s[^\]]*)?\]/i',
|
||||
function ($m) use ($items, $base) {
|
||||
$atts = array_merge($base, yang_toc_parse_atts(isset($m[1]) ? $m[1] : ''));
|
||||
return yang_toc_render($items, $atts);
|
||||
},
|
||||
$out
|
||||
);
|
||||
return is_string($replaced) ? $replaced : $content;
|
||||
}
|
||||
|
||||
/* ── ② 侧栏模式:正文只补 id,目录由 wp_footer 输出 ── */
|
||||
if ($plan['inline_at'] === null) {
|
||||
return $out;
|
||||
}
|
||||
|
||||
/* ── ③ 自动插入 ── */
|
||||
$html = yang_toc_render($items, yang_toc_atts_from_settings($plan['settings']));
|
||||
if ($html === '') {
|
||||
// 一个符合层级的标题都没有 —— 不输出空壳
|
||||
return $out;
|
||||
}
|
||||
|
||||
$pos = $plan['inline_at'];
|
||||
$slot = '<div class="yang-toc-slot yang-toc-slot-' . esc_attr($pos) . '">' . $html . '</div>';
|
||||
|
||||
return yang_toc_inject($out, $slot, $pos);
|
||||
}
|
||||
add_filter('the_content', 'yang_toc_filter_content', 9);
|
||||
|
||||
/**
|
||||
* 把目录插进正文(纯函数)
|
||||
*
|
||||
* @param string $content 已补好 id 的正文
|
||||
* @param string $slot 目录盒子
|
||||
* @param string $pos start | first | end
|
||||
* @return string
|
||||
*/
|
||||
function yang_toc_inject($content, $slot, $pos)
|
||||
{
|
||||
if ($pos === 'start') {
|
||||
return $slot . $content;
|
||||
}
|
||||
if ($pos === 'end') {
|
||||
return $content . $slot;
|
||||
}
|
||||
|
||||
/* 'first':插在第一个 H1 / H2 之前。
|
||||
· 用 h1 或 h2 是作者视角的「第一个章节标题」。文章大标题在正文之外,
|
||||
不会被算进来(见文件头注释)。
|
||||
· 用 preg_match 的 PREG_OFFSET_CAPTURE 拿字节偏移,插在**开标签之前**;
|
||||
若改成"在 <h2 后面再拆"就会把标签切断,页面结构直接坏掉。
|
||||
· 只有 h3/h4 的文章不命中 ⇒ 退回插在开头,总比不显示好。 */
|
||||
if (preg_match('/<h[12]\b/i', $content, $m, PREG_OFFSET_CAPTURE)) {
|
||||
$at = $m[0][1];
|
||||
return substr($content, 0, $at) . $slot . substr($content, $at);
|
||||
}
|
||||
|
||||
return $slot . $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* 短代码兜底
|
||||
*
|
||||
@@ -431,27 +851,124 @@ function yang_toc_shortcode($atts = array(), $content = '', $tag = '')
|
||||
}
|
||||
|
||||
$scan = yang_toc_scan($post->post_content);
|
||||
return yang_toc_render($scan['items'], is_array($atts) ? $atts : array());
|
||||
$base = yang_toc_atts_from_settings();
|
||||
$atts = is_array($atts) ? $atts : array();
|
||||
return yang_toc_render($scan['items'], array_merge($base, $atts));
|
||||
}
|
||||
add_shortcode('toc', 'yang_toc_shortcode');
|
||||
add_shortcode('TOC', 'yang_toc_shortcode');
|
||||
|
||||
/**
|
||||
* 需要时才加载样式
|
||||
* 侧边浮动目录的 HTML
|
||||
*
|
||||
* 每篇文章都加一份 TOC 样式是浪费;只有正文里真的写了 [TOC] 才加载。
|
||||
* 依赖 'kratos' 保证排在主题主样式之后,不会被覆盖。
|
||||
* 不在正文流里:一个贴边的细长标签 + 一块可滑出的面板。默认展开与否
|
||||
* 由「默认状态」决定,展开态直接写进 class(is-open),所以**没有 JS
|
||||
* 也能看到默认状态**;JS 只负责切换。
|
||||
*
|
||||
* <aside> + <button> 而不是纯 div:按钮天生可聚焦、可回车触发,键盘
|
||||
* 用户不需要额外的 tabindex / role 补丁。
|
||||
*
|
||||
* @param array $items 条目
|
||||
* @param array $plan yang_toc_plan() 的结果
|
||||
* @return string
|
||||
*/
|
||||
function yang_toc_assets()
|
||||
function yang_toc_rail_html($items, $plan)
|
||||
{
|
||||
if (is_admin() || !is_singular()) {
|
||||
$s = $plan['settings'];
|
||||
$built = yang_toc_build_list($items, yang_toc_atts_from_settings($s));
|
||||
if ($built['list'] === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$side = ($plan['side'] === 'right') ? 'right' : 'left';
|
||||
$open = ($s['default'] === 'show');
|
||||
$title = $s['title'];
|
||||
|
||||
$cls = 'yang-toc-rail yang-toc-rail-' . $side . ($open ? ' is-open' : '');
|
||||
|
||||
return '<aside class="' . esc_attr($cls) . '" data-side="' . esc_attr($side) . '"'
|
||||
. ' data-state="' . ($open ? 'open' : 'closed') . '">'
|
||||
. '<button type="button" class="yang-toc-rail-tab" aria-expanded="' . ($open ? 'true' : 'false') . '"'
|
||||
. ' aria-controls="yang-toc-rail-panel">'
|
||||
. '<span class="yang-toc-rail-tab-text">' . esc_html($title) . '</span>'
|
||||
. '</button>'
|
||||
. '<div class="yang-toc-rail-panel" id="yang-toc-rail-panel">'
|
||||
. '<div class="yang-toc-rail-head">'
|
||||
. '<span class="yang-toc-rail-title">' . esc_html($title) . '</span>'
|
||||
. '<span class="yang-toc-count">' . $built['picked'] . '</span>'
|
||||
. '<button type="button" class="yang-toc-rail-close" aria-label="' . esc_attr(__('收起目录', 'kratos')) . '">'
|
||||
. '<span aria-hidden="true">×</span>'
|
||||
. '</button>'
|
||||
. '</div>'
|
||||
. '<nav class="yang-toc-rail-body" aria-label="' . esc_attr($title) . '">'
|
||||
. $built['list']
|
||||
. '</nav>'
|
||||
. '</div>'
|
||||
. '</aside>';
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出侧边目录(页面末尾)
|
||||
*
|
||||
* 只认「当前请求就是这篇文章」这个条件,不看正文里有没有标记 ——
|
||||
* 那个判断在过滤器里已经做过一次,这里再查一次原文是必须的:
|
||||
* wp_footer 时正文可能已经被别人改过,而侧栏只关心作者写没写标记。
|
||||
*/
|
||||
function yang_toc_render_rail()
|
||||
{
|
||||
if (is_admin() || !is_singular() || is_attachment()) {
|
||||
return;
|
||||
}
|
||||
$post = get_post();
|
||||
if (!$post || !isset($post->post_content)) {
|
||||
return;
|
||||
}
|
||||
if (!has_shortcode($post->post_content, 'toc') && !has_shortcode($post->post_content, 'TOC')) {
|
||||
// 作者写了 [TOC]:手写优先,不出侧栏
|
||||
if (yang_toc_has_marker($post->post_content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$plan = yang_toc_plan(false);
|
||||
if (!$plan['rail']) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 正文没走本主题的过滤器 ⇒ 标题没有 id ⇒ 目录点不动,不如不出
|
||||
if (!yang_toc_rail_ready()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$scan = yang_toc_scan_cache($post->ID);
|
||||
echo yang_toc_rail_html($scan['items'], $plan);
|
||||
}
|
||||
add_action('wp_footer', 'yang_toc_render_rail', 20);
|
||||
|
||||
/**
|
||||
* 需要时才加载样式和脚本
|
||||
*
|
||||
* 每篇文章都加一份 TOC 资源是浪费;只有「真的会出现目录」才加载:
|
||||
* · 正文里写了 [TOC];或
|
||||
* · 后台开了自动插入(中间 + 非手动);或
|
||||
* · 后台开了侧栏(左/右 + 非手动)。
|
||||
*
|
||||
* 依赖 'kratos' 保证排在主题主样式之后,不会被覆盖。
|
||||
*/
|
||||
function yang_toc_assets()
|
||||
{
|
||||
if (is_admin() || !is_singular() || is_attachment()) {
|
||||
return;
|
||||
}
|
||||
$post = get_post();
|
||||
if (!$post || !isset($post->post_content)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* 上面已经排除了列表页/附件页,走到这里的都是单篇内容。
|
||||
自动插入和侧栏是否真的生效还要看「是不是正文的这次渲染」,
|
||||
但那要等 the_content 才知道;这里宁可多加载一份资源,
|
||||
也不能让目录出现时样式没跟上(那是肉眼可见的坏页面)。 */
|
||||
$plan = yang_toc_plan(yang_toc_has_marker($post->post_content));
|
||||
if ($plan['inline_at'] === null && !$plan['rail']) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -469,4 +986,4 @@ function yang_toc_assets()
|
||||
true
|
||||
);
|
||||
}
|
||||
add_action('wp_enqueue_scripts', 'yang_toc_assets', 20);
|
||||
add_action('wp_enqueue_scripts', 'yang_toc_assets', 20);
|
||||
|
||||
Reference in New Issue
Block a user