feat(admin): replicate reference post-page settings page 1:1

- rewrite post_list.page schema (JS+PHP 2.0.0) with the reference field set:
  three cover types with option fieldsets (image 20-200% / slide CFS_Module::slide
  full config with nested dependsOn / video 0-200%), hide-time, p_indent,
  breadcrumbs (home-text as switcher), prevnext, authordesc, maxheight slider,
  yiyan block, copyright, button toptext, front/after content, related posts
- migrate share_s to share page basic group and post_rewards_* to users
  features rewards group (runtime keys unchanged)
- remap get_post_page_settings() to reference keys, normalize slide cover
  option, hardcode non-reference runtime flags (reading progress, favorites,
  author card), drop dual-path legacy aliases
- wire slide option into aksrt_render_slider (effect/direction/spaceBetween/
  speed/interval/loop/autoplay/nav/pagination, scale vs fixed-height modes),
  clamp image scale 20-200 and video scale 0-200 with 16:9 fallback,
  wire p_indent container class + CSS, resync frontend-tags fallback and
  FacadeBulk fallback, remap PostMetaBox cover keys
- add post-page reference contract test; update stale cover-height and
  user-features contract tests

Validation: node --test 464/464 pass, audit-admin-contracts errors=0,
vite build ok, build-public-assets ok, php -l clean.
This commit is contained in:
2026-09-24 16:13:00 +08:00
parent a8060fa71d
commit 66f8bb933d
17 changed files with 797 additions and 267 deletions
@@ -0,0 +1,341 @@
'use strict';
/**
* 「文章页面」后台设置页契约(对齐参考主题 zibll 1:1):
* 1) PHP schema(SchemaPostList.php page section 2.0.0)字段键、顺序、默认值与
* JS schema(assets/admin/src/schema/sections/post-list/page.js)一致;
* 2) 参考主题字段全集存在:三类封面(article_image_cover / article_slide_cover /
* article_video_cover + option fieldset)、post_single_hide_time_s、
* post_p_indent_s、breadcrumbs_single_s、breadcrumbs_home_text(switcher)、
* post_prevnext_s、post_authordesc_s、article_maxheight_kg/maxheight、
* 一言三项、post_copyright_s/post_copyright、post_button_toptext、
* post_front_content/after_content、post_related_*;
* 3) 幻灯片 fieldset 全字段及嵌套依赖(direction/scale_height/auto_height 组合)
* 对齐参考主题 CFS_Module::slide;
* 4) 旧 AKSRT 私有/异名字段(阅读进度条、面包屑末项文本、作者卡片配置、收藏开关、
* share_s 与 post_rewards_*、旧封面键)已彻底移除,无双路兼容;
* 5) 运行时(PostDisplay::get_post_page_settings)只读取新键:幻灯片配置归一化并
* 接线 aksrt_render_slider,p_indent 接线容器类,breadcrumbs_home_text 为
* switcher 布尔语义(true = 用「首页」替代网站名称)。
*/
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const php = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaPostList.php'), 'utf8');
const js = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/post-list/page.js'), 'utf8');
const runtime = fs.readFileSync(path.join(root, 'src/Settings/PostDisplay.php'), 'utf8');
const facadeBulk = fs.readFileSync(path.join(root, 'src/Settings/FacadeBulk.php'), 'utf8');
const metaBox = fs.readFileSync(path.join(root, 'src/Core/PostMetaBox.php'), 'utf8');
const frontendTags = fs.readFileSync(path.join(root, 'inc/helpers/aksrt-frontend-tags.php'), 'utf8');
const singleRender = fs.readFileSync(path.join(root, 'inc/helpers/aksrt-single-render.php'), 'utf8');
const contentCss = fs.readFileSync(path.join(root, 'assets/css/frontend/single-post/content.css'), 'utf8');
const shareJs = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/post-list/share.js'), 'utf8');
const sharePhp = php;
const featuresJs = fs.readFileSync(path.join(root, 'assets/admin/src/schema/sections/users/features.js'), 'utf8');
const usersPhp = fs.readFileSync(path.join(root, 'src/Admin/Schema/SchemaUsers.php'), 'utf8');
/** Extract a PHP section body by its section key (balanced brackets). */
function phpSection(src, sectionKey) {
const start = src.indexOf(`'${sectionKey}' => [`);
assert.notEqual(start, -1, `PHP schema has a ${sectionKey} section`);
const open = src.indexOf('[', start);
let depth = 0;
for (let i = open; i < src.length; i++) {
const ch = src[i];
if (ch === "'" || ch === '"') {
const quote = ch;
i++;
while (i < src.length) {
if (src[i] === '\\') { i += 2; continue; }
if (src[i] === quote) break;
i++;
}
continue;
}
if (ch === '[') depth++;
else if (ch === ']') {
depth--;
if (depth === 0) return src.slice(open, i + 1);
}
}
throw new Error(`unbalanced ${sectionKey} section`);
}
const pagePhp = phpSection(php, 'page');
// 参考主题 admin-options.php 4934–5249 的字段顺序(含 fieldset 父字段与子字段)。
const PAGE_FIELD_SEQUENCE = [
'article_image_cover',
'article_image_cover_option',
'scale', // article_image_cover_option.scale
'article_slide_cover',
'article_slide_cover_option',
// article_slide_cover_option(CFS_Module::slide 全字段)
'direction', 'loop', 'button', 'pagination', 'effect', 'scale_height', 'scale',
'auto_height', 'max_height', 'min_height', 'pc_height', 'm_height',
'spacebetween', 'speed', 'autoplay', 'interval',
'article_video_cover',
'article_video_cover_option',
'scale', // article_video_cover_option.scale
'post_single_hide_time_s',
'post_p_indent_s',
'breadcrumbs_single_s',
'breadcrumbs_home_text',
'post_prevnext_s',
'post_authordesc_s',
'article_maxheight_kg',
'article_maxheight',
'yiyan_single_content_header',
'yiyan_single_content_footer',
'yiyan_single_box',
'post_copyright_s',
'post_copyright',
'post_button_toptext',
'post_front_content',
'post_after_content',
'post_related_s',
'post_related_type',
'related_title',
'post_related_n',
'post_related_orderby',
];
// 旧 AKSRT 私有/异名字段:已迁移(share_s / post_rewards_*)或无参考对应项被移除。
const LEGACY_KEYS = [
'post_reading_progress',
'post_author_card_limit',
'post_author_card_orderby',
'post_author_card_style',
'post_favorite_enabled',
'post_wechat_share',
'post_article_image_cover',
'post_article_slide_cover',
'post_article_video_cover',
];
test('PHP page section 字段键与参考主题一致且顺序相同', () => {
// 字段定义均为 ['key' => '...', 'label' => ...] 开头;dependsOn 内的
// ['key' => '...', 'value'/'not' => ...] 不是字段定义,须排除。
const found = [];
const re = /'key'\s*=>\s*'([a-z0-9_]+)',\s*'label'/g;
let m;
while ((m = re.exec(pagePhp)) !== null) found.push(m[1]);
assert.deepEqual(found, PAGE_FIELD_SEQUENCE);
});
test('PHP page section 版本升级至 2.0.0 且为单一 post_page 组', () => {
assert.match(pagePhp, /'version'\s*=>\s*'2\.0\.0'/);
assert.match(pagePhp, /'groups'\s*=>\s*\[\s*\['key'\s*=>\s*'post_page',\s*'title'\s*=>\s*'文章页面'/);
});
test('PHP page section 默认值对齐参考主题', () => {
// 三类封面开关。
assert.match(pagePhp, /'article_image_cover',\s*'label'\s*=>\s*'图片封面',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*true/);
assert.match(pagePhp, /'article_slide_cover',\s*'label'\s*=>\s*'幻灯片封面',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*true/);
assert.match(pagePhp, /'article_video_cover',\s*'label'\s*=>\s*'视频封面',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*true/);
// 图片封面比例:20-200 step5,默认 35。
assert.match(pagePhp, /'scale',\s*'label'\s*=>\s*'图片长宽比例',\s*'type'\s*=>\s*'number',\s*'default'\s*=>\s*35,\s*'min'\s*=>\s*20,\s*'max'\s*=>\s*200,\s*'step'\s*=>\s*5/);
// 视频封面比例:0-200,默认 0(为 0 则不固定长宽比例)。
assert.match(pagePhp, /'scale',\s*'label'\s*=>\s*'固定长宽比例',\s*'type'\s*=>\s*'number',\s*'default'\s*=>\s*0,\s*'min'\s*=>\s*0,\s*'max'\s*=>\s*200/);
// 面包屑「首页」替代开关(switcher,默认 true)。
assert.match(pagePhp, /'breadcrumbs_home_text',\s*'label'\s*=>\s*'面包屑导航用“首页”替代网站名称',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*true/);
// 段落缩进 / 隐藏时间。
assert.match(pagePhp, /'post_p_indent_s',\s*'label'\s*=>\s*'内容段落缩进',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*false/);
assert.match(pagePhp, /'post_single_hide_time_s',\s*'label'\s*=>\s*'隐藏时间',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*false/);
// 内容高度限制:slider 600-3000 step100。
assert.match(pagePhp, /'article_maxheight_kg',\s*'label'\s*=>\s*'内容高度限制',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*false/);
assert.match(pagePhp, /'article_maxheight',\s*'label'\s*=>\s*'限制的最大高度',\s*'type'\s*=>\s*'slider',\s*'default'\s*=>\s*1000,\s*'min'\s*=>\s*600,\s*'max'\s*=>\s*3000,\s*'step'\s*=>\s*100/);
// 版权声明。
assert.match(pagePhp, /'post_copyright_s',\s*'label'\s*=>\s*'版权声明',\s*'type'\s*=>\s*'switcher',\s*'default'\s*=>\s*true/);
assert.match(pagePhp, /'post_copyright',\s*'label'\s*=>\s*'版权提示内容',\s*'type'\s*=>\s*'textarea',\s*'default'\s*=>\s*'文章版权归作者所有,未经允许请勿转载。'/);
assert.match(pagePhp, /'post_button_toptext',\s*'label'\s*=>\s*'文章页脚文案',\s*'type'\s*=>\s*'input',\s*'default'\s*=>\s*'喜欢就支持一下吧'/);
// 相关文章:image_select 三样式 + 数量 4-12 step2 + 排序默认 views。
assert.match(pagePhp, /'post_related_type',\s*'label'\s*=>\s*'显示样式',\s*'type'\s*=>\s*'image_select',\s*'default'\s*=>\s*'img'/);
assert.match(pagePhp, /'post_related_n',\s*'label'\s*=>\s*'显示数量',\s*'type'\s*=>\s*'number',\s*'default'\s*=>\s*6,\s*'min'\s*=>\s*4,\s*'max'\s*=>\s*12,\s*'step'\s*=>\s*2/);
assert.match(pagePhp, /'post_related_orderby',\s*'label'\s*=>\s*'排序方式',\s*'type'\s*=>\s*'select',\s*'default'\s*=>\s*'views'/);
});
test('PHP slide fieldset 全字段及嵌套依赖对齐 CFS_Module::slide', () => {
// 幻灯片默认值(pagination: true, pc_height: 380, m_height: 180 为参考主题默认)。
assert.match(pagePhp, /'direction'\s*=>\s*'horizontal',\s*'loop'\s*=>\s*true,\s*'button'\s*=>\s*false,\s*'pagination'\s*=>\s*true,\s*'effect'\s*=>\s*'slide'/);
assert.match(pagePhp, /'pc_height'\s*=>\s*380,\s*'m_height'\s*=>\s*180,\s*'spacebetween'\s*=>\s*15,\s*'speed'\s*=>\s*0,\s*'autoplay'\s*=>\s*true,\s*'interval'\s*=>\s*4/);
// 嵌套依赖:scale_height 需要 direction != vertical。
assert.match(pagePhp, /'scale_height',\s*'label'\s*=>\s*'按比例自动高度',\s*'type'\s*=>\s*'switch',\s*'default'\s*=>\s*true,\s*'dependsOn'\s*=>\s*\['key'\s*=>\s*'direction',\s*'not'\s*=>\s*'vertical'\]/);
// scale 需要 direction != vertical 且 scale_height 开启。
assert.match(pagePhp, /'scale',\s*'label'\s*=>\s*'长宽比例'[\s\S]*?'dependsOn'\s*=>\s*\['all'\s*=>\s*\[\['key'\s*=>\s*'direction',\s*'not'\s*=>\s*'vertical'\],\s*\['key'\s*=>\s*'scale_height',\s*'value'\s*=>\s*true\]\]\]/);
// auto_height 需要 direction != vertical 且 scale_height 关闭。
assert.match(pagePhp, /'auto_height',\s*'label'\s*=>\s*'自动高度'[\s\S]*?'dependsOn'\s*=>\s*\['all'\s*=>\s*\[\['key'\s*=>\s*'direction',\s*'not'\s*=>\s*'vertical'\],\s*\['key'\s*=>\s*'scale_height',\s*'value'\s*=>\s*false\]\]\]/);
// max/min_height 需要 auto_height 开启 + direction != vertical + scale_height 关闭。
assert.match(pagePhp, /'max_height',\s*'label'\s*=>\s*'最大高度'[\s\S]*?'dependsOn'\s*=>\s*\['all'\s*=>\s*\[\['key'\s*=>\s*'auto_height',\s*'value'\s*=>\s*true\],\s*\['key'\s*=>\s*'direction',\s*'not'\s*=>\s*'vertical'\],\s*\['key'\s*=>\s*'scale_height',\s*'value'\s*=>\s*false\]\]\]/);
// interval 依赖 autoplay。
assert.match(pagePhp, /'interval',\s*'label'\s*=>\s*'停顿时间'[\s\S]*?'dependsOn'\s*=>\s*\['key'\s*=>\s*'autoplay',\s*'value'\s*=>\s*true\]/);
// 五种切换动画。
for (const effect of ['slide', 'fade', 'cube', 'coverflow', 'flip']) {
assert.ok(pagePhp.includes(`'value' => '${effect}'`), `缺少切换动画 ${effect}`);
}
});
test('PHP page section 不含旧私有/异名字段(无双路)', () => {
for (const legacy of LEGACY_KEYS) {
assert.ok(!pagePhp.includes(`'${legacy}'`), `page section 残留旧字段: ${legacy}`);
}
// share_s / post_rewards_* 已迁移至分享设置页与用户功能页。
assert.ok(!pagePhp.includes(`'share_s'`), 'page section 残留 share_s');
assert.ok(!pagePhp.includes(`'post_rewards_`), 'page section 残留 post_rewards_*');
});
test('JS schema 与 PHP 字段键一致(ui_only 一言行除外)', () => {
const keys = [];
const re = /key:\s*'([a-z0-9_]+)',\s*label:/g;
let m;
while ((m = re.exec(js)) !== null) keys.push(m[1]);
assert.deepEqual(keys, PAGE_FIELD_SEQUENCE);
for (const legacy of LEGACY_KEYS) {
assert.ok(!keys.includes(legacy), `JS schema 残留旧字段: ${legacy}`);
}
assert.ok(!js.includes(`key: 'share_s'`), 'JS page schema 残留 share_s');
assert.ok(!/key:\s*'post_rewards_/.test(js), 'JS page schema 残留 post_rewards_*');
// 参考主题视频封面配置教程链接以 ui_only description 承载。
assert.match(js, /ui_only:\s*true/);
assert.match(js, /https:\/\/www\.zibll\.com\/1900\.html/);
});
test('JS schema 默认值与依赖对齐参考主题', () => {
assert.match(js, /article_image_cover:\s*true/);
assert.match(js, /article_image_cover_option:\s*\{\s*scale:\s*35\s*\}/);
assert.match(js, /article_slide_cover:\s*true/);
assert.match(js, /article_video_cover:\s*true/);
assert.match(js, /article_video_cover_option:\s*\{\s*scale:\s*0\s*\}/);
assert.match(js, /post_single_hide_time_s:\s*false/);
assert.match(js, /post_p_indent_s:\s*false/);
assert.match(js, /breadcrumbs_single_s:\s*true/);
assert.match(js, /breadcrumbs_home_text:\s*true/);
assert.match(js, /post_prevnext_s:\s*true/);
assert.match(js, /post_authordesc_s:\s*true/);
assert.match(js, /article_maxheight_kg:\s*false/);
assert.match(js, /article_maxheight:\s*1000/);
assert.match(js, /post_copyright_s:\s*true/);
assert.match(js, /post_copyright:\s*'文章版权归作者所有,未经允许请勿转载。'/);
assert.match(js, /post_button_toptext:\s*'喜欢就支持一下吧'/);
assert.match(js, /post_related_s:\s*true/);
assert.match(js, /post_related_type:\s*'img'/);
assert.match(js, /related_title:\s*'相关推荐'/);
assert.match(js, /post_related_n:\s*6/);
assert.match(js, /post_related_orderby:\s*'views'/);
// 幻灯片嵌套依赖(dependsOn {key, not} / {all: [...]} 形式)。
assert.match(js, /key:\s*'scale_height',[\s\S]*?dependsOn:\s*\{\s*key:\s*'direction',\s*not:\s*'vertical'\s*\}/);
assert.match(js, /key:\s*'scale',[\s\S]*?dependsOn:\s*\{\s*all:\s*\[\{\s*key:\s*'direction',\s*not:\s*'vertical'\s*\},\s*\{\s*key:\s*'scale_height',\s*value:\s*true\s*\}\]\s*\}/);
assert.match(js, /key:\s*'auto_height',[\s\S]*?dependsOn:\s*\{\s*all:\s*\[\{\s*key:\s*'direction',\s*not:\s*'vertical'\s*\},\s*\{\s*key:\s*'scale_height',\s*value:\s*false\s*\}\]\s*\}/);
assert.match(js, /key:\s*'interval',[\s\S]*?dependsOn:\s*\{\s*key:\s*'autoplay',\s*value:\s*true\s*\}/);
});
test('share_s 迁移至分享设置页 basic 组(键不变,无双路)', () => {
assert.match(sharePhp, /\['key' => 'basic', 'title' => '分享按钮', 'fields' => \[\s*\['key' => 'share_s', 'label' => '分享按钮', 'type' => 'switcher', 'default' => true, 'help' => '关闭后,在文章页面将不显示分享按钮,论坛及商城不受影响'\]/);
assert.match(shareJs, /key: 'basic',/);
assert.match(shareJs, /share_s: true/);
assert.match(shareJs, /key: 'share_s', label: '分享按钮', type: 'switch', default: true, help: '关闭后,在文章页面将不显示分享按钮,论坛及商城不受影响'/);
});
test('post_rewards_* 迁移至用户功能页 rewards 组(对齐参考主题 8977 行)', () => {
assert.match(usersPhp, /\['key' => 'rewards', 'title' => '打赏', 'fields' => \[/);
assert.match(usersPhp, /'post_rewards_s', 'label' => '用户打赏功能', 'type' => 'switcher', 'default' => true/);
assert.match(usersPhp, /'post_rewards_text', 'label' => '打赏按钮文案', 'type' => 'input', 'default' => '赞赏'/);
assert.match(featuresJs, /post_rewards_s:\s*true/);
assert.match(featuresJs, /post_rewards_text:\s*'赞赏'/);
assert.match(featuresJs, /key: 'post_rewards_s', label: '用户打赏功能', type: 'switch', default: true/);
});
test('旧字段在 schema 与运行时中均已移除(无双路)', () => {
for (const legacy of LEGACY_KEYS) {
assert.ok(!php.includes(legacy), `PHP schema 残留旧字段: ${legacy}`);
assert.ok(!runtime.includes(legacy), `PostDisplay 残留旧字段: ${legacy}`);
assert.ok(!js.includes(legacy), `page.js 残留旧字段: ${legacy}`);
assert.ok(!metaBox.includes(legacy), `PostMetaBox 残留旧字段: ${legacy}`);
}
// 面包屑末项正文无参考配置:schema 字段移除,运行时硬编码为常开。
assert.ok(!pagePhp.includes(`'breadcrumbs_single_text'`), 'page section 残留 breadcrumbs_single_text');
assert.ok(!js.includes(`key: 'breadcrumbs_single_text'`), 'page.js 残留 breadcrumbs_single_text');
assert.match(runtime, /'breadcrumbs_single_text'\s*=>\s*true/);
// aksrt-frontend-tags 不再有 breadcrumbs_home_text 文本兼容(switcher 布尔语义)。
assert.ok(!frontendTags.includes("breadcrumbs_home'] = (string) $settings['breadcrumbs_home_text'"), 'frontend-tags 残留 breadcrumbs_home_text 文本兼容');
});
test('运行时只读取新键并接线封面/缩进/版权/一言', () => {
// 三类封面 + option fieldset。
assert.match(runtime, /\$settings\['article_image_cover'\]/);
assert.match(runtime, /\$settings\['article_image_cover_option'\]/);
assert.match(runtime, /\$settings\['article_slide_cover'\]/);
assert.match(runtime, /\$settings\['article_slide_cover_option'\]/);
assert.match(runtime, /\$settings\['article_video_cover'\]/);
assert.match(runtime, /\$settings\['article_video_cover_option'\]/);
// 图片封面比例 clamp 20-200(参考主题默认 35)。
assert.match(runtime, /max\(20, min\(200, \(int\) \(\$image_cover_option\['scale'\] \?\? 35\)\)\)/);
// 视频封面比例 clamp 0-200(0 = 不固定长宽比例)。
assert.match(runtime, /max\(0, min\(200, \(int\) \(\$video_cover_option\['scale'\] \?\? 0\)\)\)/);
// 幻灯片配置归一化(CFS_Module::slide 全字段)。
assert.match(runtime, /normalize_slide_cover_option\(\$slide_option_raw\)/);
for (const key of ['direction', 'loop', 'button', 'pagination', 'effect', 'scale_height', 'scale', 'auto_height', 'max_height', 'min_height', 'pc_height', 'm_height', 'spacebetween', 'speed', 'autoplay', 'interval']) {
assert.ok(runtime.includes(`'${key}'`), `normalize_slide_cover_option 缺少 ${key}`);
}
// 面包屑「首页」替代语义:true → 「首页」,false → 网站名称。
assert.match(runtime, /!isset\(\$settings\['breadcrumbs_home_text'\]\) \|\| !empty\(\$settings\['breadcrumbs_home_text'\]\)/);
assert.match(runtime, /get_bloginfo\('name'\)/);
// 段落缩进 / 高度限制 / 版权 / 页脚文案 / 插入内容 / 一言 / 相关文章。
assert.match(runtime, /\$settings\['post_p_indent_s'\]/);
assert.match(runtime, /\$settings\['article_maxheight_kg'\]/);
assert.match(runtime, /\$settings\['article_maxheight'\]/);
assert.match(runtime, /\$settings\['post_copyright_s'\]/);
assert.match(runtime, /\$settings\['post_copyright'\]/);
assert.match(runtime, /\$settings\['post_button_toptext'\]/);
assert.match(runtime, /\$settings\['post_front_content'\]/);
assert.match(runtime, /\$settings\['post_after_content'\]/);
assert.match(runtime, /\$settings\['yiyan_single_content_header'\]/);
assert.match(runtime, /\$settings\['yiyan_single_content_footer'\]/);
assert.match(runtime, /\$settings\['yiyan_single_box'\]/);
assert.match(runtime, /\$settings\['post_related_s'\]/);
assert.match(runtime, /\$settings\['post_related_type'\]/);
assert.match(runtime, /\$settings\['post_related_n'\]/);
assert.match(runtime, /\$settings\['post_related_orderby'\]/);
// 阅读进度/收藏/作者卡片无参考配置:常量默认(无双路)。
assert.match(runtime, /'reading_progress'\s*=>\s*true/);
assert.match(runtime, /'favorite_enabled'\s*=>\s*true/);
assert.match(runtime, /'author_card_limit'\s*=>\s*6/);
});
test('前台链路:幻灯片配置接线渲染器,p_indent 接线容器与 CSS', () => {
// 幻灯片配置接入 aksrt_render_slider(effect/direction/spaceBetween/speed/interval)。
assert.match(singleRender, /article_slide_cover_option/);
assert.match(singleRender, /'effect'\s*=>\s*\(string\) \(\$slide_option\['effect'\] \?\? 'slide'\)/);
assert.match(singleRender, /'direction'\s*=>\s*\(string\) \(\$slide_option\['direction'\] \?\? 'horizontal'\)/);
assert.match(singleRender, /'spaceBetween'\s*=>\s*\(int\) \(\$slide_option\['spacebetween'\] \?\? 15\)/);
assert.match(singleRender, /'interval'\s*=>\s*max\(1, \(int\) \(\$slide_option\['interval'\] \?\? 4\)\) \* 1000/);
assert.match(singleRender, /'show_nav'\s*=>\s*!empty\(\$slide_option\['button'\]\)/);
assert.match(singleRender, /'show_pagination'\s*=>\s*!empty\(\$slide_option\['pagination'\]\)/);
// 固定高度模式(scale_height)与比例高度模式(scale + 50px 标题预留)双分支。
assert.match(singleRender, /!empty\(\$slide_option\['scale_height'\]\)/);
assert.match(singleRender, /\$slider_args\['pc_height'\] = \(\(int\) \(\$slide_option\['pc_height'\] \?\? 380\)\) \. 'px'/);
assert.match(singleRender, /\$slider_args\['scale_ratio'\] = max\(10, \(int\) \(\$slide_option\['scale'\] \?\? 35\)\) \/ 100/);
// 图片封面比例 clamp 20-200;视频封面 0 → 16:9(56.25)回退。
assert.match(singleRender, /max\(20, min\(200, \(int\) \$ctx\['img_scale'\]\)\)/);
assert.match(singleRender, /max\(0, min\(200, \(int\) \$ctx\['video_scale'\]\)\)/);
assert.match(singleRender, /\$video_scale > 0 \? \(string\) \$video_scale : '56\.25'/);
// p_indent 容器类 + CSS 规则(首行缩进 2em)。
assert.match(singleRender, /p-indent/);
assert.match(contentCss, /\.wp-posts-content\.p-indent p\s*\{\s*text-indent:\s*2em;/);
// 前端回退数组包含新键。
assert.match(frontendTags, /'p_indent'\s*=>\s*false/);
assert.match(frontendTags, /'article_image_cover_scale'\s*=>\s*35/);
assert.match(frontendTags, /'article_slide_cover_option'\s*=>\s*\[\]/);
assert.match(frontendTags, /'article_video_cover_scale'\s*=>\s*0/);
// PostMetaBox 读取新封面键(文章级封面字段可见性)。
assert.match(metaBox, /!empty\(\$settings\['article_image_cover'\]\)/);
assert.match(metaBox, /!empty\(\$settings\['article_slide_cover'\]\)/);
assert.match(metaBox, /!empty\(\$settings\['article_video_cover'\]\)/);
// FacadeBulk 独立回退与新键同步。
assert.match(facadeBulk, /'breadcrumbs_home_replace'/);
assert.match(facadeBulk, /'article_slide_cover_option'/);
});