<?php

/**
 * EnhancedMarkdown - 增强版Markdown解析器插件
 *
 * 基于 Parsedown + ParsedownExtra 解析引擎，支持更全面的 Markdown 语法。
 * 完全替换系统默认 HyperDown 解析器，从根本上解决语法支持不足和显示故障问题。
 *
 * 支持的语法：
 * - 标准 Markdown：标题/加粗/斜体/链接/图片/代码/列表/引用/分割线
 * - GFM 扩展：表格（完整）、删除线 ~~text~~、任务列表 - [x] / - [ ]
 * - Extra 扩展：脚注 [^1]、定义列表、缩写词 *[ABBR]: definition
 * - 自定义扩展：上标 ^text^、下标 ~text~、高亮 ==text==
 * - 自定义扩展：TOC [toc]、容器 :::type、图片尺寸 ![alt](url =WxH)
 * - 自定义扩展：数学公式 $...$ 和 $$...$$（KaTeX 前端渲染）
 * - 自定义扩展：Mermaid 图表 ```mermaid（Mermaid.js 前端渲染）
 * - 自定义扩展：代码语法高亮（Prism.js 前端渲染，按需加载）
 *
 * @package EnhancedMarkdown
 * @author 首政君
 * @version 3.9.1
 * @link https://blog.qgdnmb.xyz/2026/05/26/Typecho-Markdown.html
 */

namespace TypechoPlugin\EnhancedMarkdown;

use Typecho\Plugin\PluginInterface;
use Typecho\Widget\Helper\Form;
use Typecho\Widget\Helper\Form\Element\Radio;
use Typecho\Widget\Helper\Form\Element\Text;
use Widget\Options;

if (!defined('__TYPECHO_ROOT_DIR__')) {
    exit;
}

// 修复 #4：将 require_once 移至安全检查之后，避免在未验证环境下加载外部文件
// Parsedown 解析引擎（在命名空间外加载，确保全局可用）
require_once __DIR__ . '/Parsedown.php';
require_once __DIR__ . '/ParsedownExtra.php';

/**
 * EnhancedMarkdown 插件主入口类
 *
 * 注册 markdown hook 替换系统默认解析器，
 * 同时注册 header hook 注入增强语法CSS样式。
 */
class Plugin implements PluginInterface
{
    /**
     * 插件资产文件基础路径（相对于插件目录）
     *
     * KaTeX 和 Mermaid 的 JS/CSS 文件存放于此目录下。
     *
     * @since v3.3.0 新增
     */
    const ASSETS_SUBDIR = 'usr/plugins/EnhancedMarkdown';

    /**
     * 按需加载标志：当前请求是否包含数学公式元素
     *
     * 在 parse() 中检测解析结果是否包含 .math-block / .math-inline 元素，
     * injectScripts() 据此决定是否加载 KaTeX 资源。
     * 每次请求自动重置（PHP 请求生命周期）。
     *
     * @since v3.4.0 新增
     * @var bool
     */
    private static $hasMath = false;

    /**
     * 按需加载标志：当前请求是否包含 Mermaid 图表元素
     *
     * 在 parse() 中检测解析结果是否包含 class="mermaid" 元素，
     * injectScripts() 据此决定是否加载 Mermaid.js 资源。
     * 每次请求自动重置（PHP 请求生命周期）。
     *
     * @since v3.4.0 新增
     * @var bool
     */
    private static $hasMermaid = false;

    /**
     * 按需加载标志：当前请求是否包含代码块元素
     *
     * 在 parse() 中检测解析结果是否包含 <code class="language-xxx"> 元素，
     * injectStyles() / injectScripts() 据此决定是否加载 Prism.js 资源。
     * 每次请求自动重置（PHP 请求生命周期）。
     *
     * @since v3.6.0 新增
     * @var bool
     */
    private static $hasCodeBlock = false;

    /**
     * 激活插件
     *
     * 注册以下 Hook：
     * - Widget\Base\Contents → markdown：替换文章 Markdown 解析器
     * - Widget\Base\Comments → markdown：替换评论 Markdown 解析器
     * - Widget\Archive → header：注入增强语法 CSS 样式 + Prism.js CSS（按需）
     * - Widget\Archive → footer：注入 KaTeX/Mermaid/Prism.js JS 脚本（按需）
     *
     * @static
     * @access public
     * @return void
     */
    public static function activate()
    {
        // 修复 #8：Hook 注册改用数组格式（IDE 支持跳转、静态分析工具可检测）
        // 文章 Markdown 解析 hook（替换默认 Markdown::convert）
        \Typecho\Plugin::factory('Widget\Base\Contents')->markdown = [__CLASS__, 'parse'];

        // 评论 Markdown 解析 hook（替换默认 Markdown::convert）
        \Typecho\Plugin::factory('Widget\Base\Comments')->markdown = [__CLASS__, 'parse'];

        // 自定义 CSS 输出到文章页面头部
        \Typecho\Plugin::factory('Widget\Archive')->header = [__CLASS__, 'injectStyles'];

        // JS 脚本输出到文章页面底部（KaTeX、Mermaid 按需加载）
        \Typecho\Plugin::factory('Widget\Archive')->footer = [__CLASS__, 'injectScripts'];
    }

    /**
     * 停用插件
     *
     * @static
     * @access public
     * @return void
     */
    public static function deactivate()
    {
        // 停用时自动移除所有钩子（Typecho机制）
    }

    /**
     * 插件配置面板
     *
     * @access public
     * @param Form $form 配置表单
     * @return void
     */
    public static function config(Form $form)
    {
        // TOC目录开关
        $toc = new Radio(
            'toc',
            array('1' => _t('启用'), '0' => _t('禁用')),
            '1',
            _t('TOC目录支持'),
            _t('启用后可使用 [toc] 标记自动生成文章目录，标题自动添加锚点ID')
        );
        $form->addInput($toc);

        // 任务列表开关
        $taskList = new Radio(
            'taskList',
            array('1' => _t('启用'), '0' => _t('禁用')),
            '1',
            _t('任务列表支持'),
            _t('启用后支持 - [x] 和 - [ ] 语法生成复选框列表')
        );
        $form->addInput($taskList);

        // 上标开关
        $sup = new Radio(
            'sup',
            array('1' => _t('启用'), '0' => _t('禁用')),
            '1',
            _t('上标语法支持'),
            _t('启用后支持 ^text^ 语法生成上标 <sup>text</sup>')
        );
        $form->addInput($sup);

        // 下标开关
        $sub = new Radio(
            'sub',
            array('1' => _t('启用'), '0' => _t('禁用')),
            '1',
            _t('下标语法支持'),
            _t('启用后支持 ~text~ 语法生成下标 <sub>text</sub>（注意与删除线 ~~text~~ 的区别）')
        );
        $form->addInput($sub);

        // 高亮标记开关
        $mark = new Radio(
            'mark',
            array('1' => _t('启用'), '0' => _t('禁用')),
            '1',
            _t('高亮标记支持'),
            _t('启用后支持 ==text== 语法生成高亮标记 <mark>text</mark>')
        );
        $form->addInput($mark);

        // 容器/提示块开关
        $container = new Radio(
            'container',
            array('1' => _t('启用'), '0' => _t('禁用')),
            '1',
            _t('容器/提示块支持'),
            _t('启用后支持 :::tip、:::info、:::warning、:::danger、:::note、:::success 等容器块语法')
        );
        $form->addInput($container);

        // 图片尺寸开关
        $imageSize = new Radio(
            'imageSize',
            array('1' => _t('启用'), '0' => _t('禁用')),
            '1',
            _t('图片尺寸支持'),
            _t('启用后支持 ![alt](url =WxH) 语法设置图片宽高')
        );
        $form->addInput($imageSize);

        // 数学公式开关
        $math = new Radio(
            'math',
            array('1' => _t('启用'), '0' => _t('禁用')),
            '1',
            _t('数学公式支持（KaTeX）'),
            _t('启用后支持 $...$ 行内公式和 $$...$$ 块级公式，由 KaTeX 前端渲染')
        );
        $form->addInput($math);

        // Mermaid 图表开关
        $mermaid = new Radio(
            'mermaid',
            array('1' => _t('启用'), '0' => _t('禁用')),
            '1',
            _t('Mermaid 图表支持'),
            _t('启用后支持 ```mermaid 代码块渲染流程图、时序图、甘特图等')
        );
        $form->addInput($mermaid);

        // 代码语法高亮开关
        // v3.6.0 新增：内置 Prism.js 语法高亮，下载版已包含全部语言支持
        // 按需加载：仅当代码块存在时才加载 Prism.js 资源，无代码块的页面零开销
        $highlight = new Radio(
            'highlight',
            array('1' => _t('启用'), '0' => _t('禁用')),
            '1',
            _t('代码语法高亮（Prism.js）'),
            _t('启用后自动为 ```language 代码块添加语法着色，支持 200+ 编程语言。按需加载：仅当代码块存在时才加载资源')
        );
        $form->addInput($highlight);

        // KaTeX 资源来源选择
        // v3.8.0 新增：允许用户选择使用插件本地文件或 CDN 网址加载 KaTeX 资源
        // 默认使用插件本地文件，CDN 模式下用户可自定义基础路径（只需输入到目录级别）
        $katexSource = new Radio(
            'katexSource',
            array('local' => _t('插件本地文件'), 'cdn' => _t('CDN 网址')),
            'local',
            _t('KaTeX 资源来源'),
            _t('选择 KaTeX 数学公式渲染库的加载方式。插件本地文件无需外部网络请求；CDN 模式可减轻服务器带宽压力，需输入 CDN 基础路径')
        );
        $form->addInput($katexSource);

        // KaTeX CDN 基础路径（仅在资源来源选择"CDN 网址"时生效）
        // 用户只需输入到目录级别路径（以 / 结尾），插件自动拼接 katex.min.css、katex.min.js、auto-render.min.js
        $katexCdnBase = new Text(
            'katexCdnBase',
            NULL,
            'https://cdn.jsdelivr.net/npm/katex@0.17.0/dist/',
            _t('KaTeX CDN 基础路径'),
            _t('仅在 KaTeX 资源来源选择"CDN 网址"时生效。输入 CDN 目录路径（以 / 结尾），插件将自动拼接 katex.min.css、katex.min.js、auto-render.min.js 等文件名')
        );
        $form->addInput($katexCdnBase);

        // Mermaid 资源来源选择
        // v3.8.0 新增：允许用户选择使用插件本地文件或 CDN 网址加载 Mermaid 资源
        $mermaidSource = new Radio(
            'mermaidSource',
            array('local' => _t('插件本地文件'), 'cdn' => _t('CDN 网址')),
            'local',
            _t('Mermaid 资源来源'),
            _t('选择 Mermaid 图表渲染库的加载方式。插件本地文件无需外部网络请求；CDN 模式可减轻服务器带宽压力，需输入 CDN 基础路径')
        );
        $form->addInput($mermaidSource);

        // Mermaid CDN 基础路径（仅在资源来源选择"CDN 网址"时生效）
        // 用户只需输入到目录级别路径（以 / 结尾），插件自动拼接 mermaid.min.js
        $mermaidCdnBase = new Text(
            'mermaidCdnBase',
            NULL,
            'https://cdn.jsdelivr.net/npm/mermaid@11.15.0/dist/',
            _t('Mermaid CDN 基础路径'),
            _t('仅在 Mermaid 资源来源选择"CDN 网址"时生效。输入 CDN 目录路径（以 / 结尾），插件将自动拼接 mermaid.min.js 文件名')
        );
        $form->addInput($mermaidCdnBase);

        // 标题 ID 格式开关（仅在 TOC 目录启用时生效）
        // v3.5.2 新增：允许管理员在 slug 格式（可读、可预测）和旧版数字格式（toc-N）之间切换
        // 关闭 TOC 目录后此选项无效（标题不会生成任何 ID）
        $slugId = new Radio(
            'slugId',
            array('1' => _t('Slug 格式（推荐）'), '0' => _t('数字格式（toc-N）')),
            '1',
            _t('标题锚点 ID 格式'),
            _t('仅在 TOC 目录启用时生效。Slug 格式根据标题文本生成可读 ID（如 "hello-world"、"安装步骤"），便于分享和锚点链接；数字格式按解析顺序生成 toc-1、toc-2 等。关闭 TOC 后标题不生成任何 ID，此选项无效')
        );
        $form->addInput($slugId);
    }

    /**
     * 个人用户的配置面板（本插件无此需求）
     *
     * @access public
     * @param Form $form
     * @return void
     */
    public static function personalConfig(Form $form)
    {
        // 无个人配置项
    }

    /**
     * 解析 Markdown 文本
     *
     * 挂载到 markdown hook，替换系统默认的 Markdown::convert()。
     * markdown hook 签名：filter('markdown', $text)
     * Typecho filter() 会传递 ($text, $lastResult) 两个参数。
     *
     * @static
     * @access public
     * @param string|null $text Markdown 原始文本
     * @param string|null $lastResult 上一个 filter 的返回值（修复 #6：补全签名）
     * @return string 解析后的 HTML
     */
    public static function parse($text, $lastResult = null)
    {
        require_once __DIR__ . '/Parser.php';

        // 修复 #5：使用静态缓存避免每次调用新建 Parser 实例
        // 首页等多文章列表页面对同一请求可复用实例，减少构造函数开销
        static $parser;
        if (empty($parser)) {
            $parser = new Parser();
        }

        // 每次调用时读取最新配置并重置 TOC 状态
        $options = Options::alloc()->plugin('EnhancedMarkdown');

        $parser->configure(array(
            'toc'       => $options->toc ?: '0',
            'taskList'  => $options->taskList ?: '0',
            'sup'       => $options->sup ?: '0',
            'sub'       => $options->sub ?: '0',
            'mark'      => $options->mark ?: '0',
            'container' => $options->container ?: '0',
            'imageSize' => $options->imageSize ?: '0',
            'math'      => $options->math ?: '0',
            'mermaid'   => $options->mermaid ?: '0',
            'slugId'    => $options->slugId ?: '0',
            'highlight' => $options->highlight ?: '0',
        ));

        // 初始化解析器（启用HTML、注册Hook）
        $parser->init();

        $html = $parser->makeHtml($text);

        // 按需加载检测：扫描解析结果中是否包含数学公式和 Mermaid 图表元素
        // 设置静态标志供 injectStyles() / injectScripts() 读取，
        // 避免在不含公式/图表的页面加载 KaTeX（~1.5MB）或 Mermaid（3.2MB）
        // @since v3.4.0 新增
        if ($options->math !== '0') {
            self::$hasMath = self::$hasMath
                || strpos($html, 'class="math-block"') !== false
                || strpos($html, 'class="math-inline"') !== false;
        }
        if ($options->mermaid !== '0') {
            self::$hasMermaid = self::$hasMermaid
                || strpos($html, 'class="mermaid"') !== false;
        }

        // 按需加载检测：扫描解析结果中是否包含代码块元素
        // 在 lang- 前缀转换之前检测 language- 前缀（Parsedown 原始输出格式）
        // Prism.js 同时匹配 lang-xxx 和 language-xxx，转换后仍可正常高亮
        // @since v3.6.0 新增
        if ($options->highlight !== '0') {
            self::$hasCodeBlock = self::$hasCodeBlock
                || preg_match('/<code class="language-[\w-]+"/i', $html);
        }

        // 修复 #3：兼容系统 lang- 前缀约定
        // 系统默认 Markdown::convert 会将 <code class="php"> 转为 <code class="lang-php">
        // Parsedown 输出 <code class="language-php">，需转换为 lang- 前缀
        // 以确保现有语法高亮插件（Prism.js、Highlight.js 适配插件）正常工作
        // 修正：正则从 \w+ 改为 [\w-]+，支持含连字符的语言标识（如 objective-c、c++）
        // 注意：Prism.js 同时匹配 lang-xxx 和 language-xxx 前缀，转换后不影响高亮
        $html = preg_replace(
            '/<code class="language-([\w-]+)"/i',
            '<code class="lang-$1"',
            $html
        );

        // 修复 #9：为 <pre> 元素添加 line-numbers 类，激活 Prism.js 行号显示插件
        // Prism.js line-numbers 插件要求 <pre> 或 <code> 元素包含 line-numbers CSS 类
        // 未添加时 Prism.util.isActive() 返回 false，行号不显示
        // 修改日期：2026-05-27 修改原因：v3.6.0 行号功能未生效
        // 修改日期：2026-05-27 修改原因：v3.7.0 适配暗色主题 actual-code-content 结构
        $html = preg_replace(
            '/<pre class="actual-code-content"><code class="lang-/',
            '<pre class="actual-code-content line-numbers"><code class="lang-',
            $html
        );

        // 处理 <!--more--> 标记（与默认 Markdown::convert 行为一致）
        return str_replace(
            '<p><!--more--></p>',
            '<!--more-->',
            $html
        );
    }

    /**
     * 获取资源文件的完整 URL（本地或 CDN）
     *
     * 根据插件配置的资源来源选项，返回本地插件路径或 CDN 基础路径拼接后的完整 URL。
     * 用户在后台配置 CDN 基础路径时只需输入到目录级别（如 https://cdn.jsdelivr.net/npm/katex@0.17.0/dist/），
     * 插件自动拼接具体文件名（如 katex.min.js）。
     *
     * @param string $sourceOption 资源来源配置项名称（如 'katexSource'）
     * @param string $cdnBaseOption CDN 基础路径配置项名称（如 'katexCdnBase'）
     * @param string $localPath 本地相对路径（如 '/katex/katex.min.js'）
     * @param string $fileName 文件名（如 'katex.min.js'），用于拼接 CDN 路径
     * @return string 完整的资源 URL
     * @since v3.8.0 新增
     */
    private static function getResourceUrl(string $sourceOption, string $cdnBaseOption, string $localPath, string $fileName): string
    {
        $options = Options::alloc()->plugin('EnhancedMarkdown');
        $source = $options->$sourceOption ?: 'local';

        // CDN 模式：拼接用户配置的基础路径 + 文件名
        if ($source === 'cdn') {
            $cdnBase = $options->$cdnBaseOption ?: '';
            // 安全回退：CDN 基础路径为空时回退到本地文件，避免生成无效 URL
            if (empty($cdnBase)) {
                return '/' . self::ASSETS_SUBDIR . $localPath;
            }
            return rtrim($cdnBase, '/') . '/' . $fileName;
        }

        // 默认：使用插件本地文件
        return '/' . self::ASSETS_SUBDIR . $localPath;
    }

    /**
     * 注入增强语法的 CSS 样式
     *
     * 挂载到文章页面头部，输出任务列表、TOC、容器、高亮等增强语法的样式。
     * Archive header hook 通过 call() 调用，传递 ($header, $archive) 参数。
     *
     * @static
     * @access public
     * @param string|null $header 页面 header 内容（修复 #6：补全签名）
     * @param \Widget\Archive|null $archive 当前文章归档对象
     * @return void
     */
    public static function injectStyles($header = null, $archive = null)
    {
        // 增强语法基础样式（轻量，始终输出）
        $css = <<<'STYLE'
<style>
/* EnhancedMarkdown 增强语法样式 */

/* 任务列表 */
.task-list-item {
    list-style: none !important;
    margin-left: -1.5em;
}
.task-list-checkbox {
    margin-right: 0.5em;
    vertical-align: middle;
    position: relative;
    top: -1px;
}

/* TOC目录 */
.toc {
    background: #f8f9fa;
    border: 1px solid #e9ecef;
    border-radius: 6px;
    padding: 15px 20px;
    margin-bottom: 20px;
}
.toc-title {
    font-weight: bold;
    font-size: 1.1em;
    margin: 0 0 10px 0 !important;
    color: #495057;
}
.toc ul {
    list-style: none !important;
    padding-left: 0 !important;
    margin: 0 !important;
}
.toc-list {
    counter-reset: toc-level;
}
.toc-sublist {
    padding-left: 20px !important;
    margin-top: 4px !important;
}
.toc-item {
    padding: 3px 0;
}
.toc-item a {
    color: #0366d6;
    text-decoration: none;
    font-size: 0.95em;
    line-height: 1.6;
}
.toc-item a:hover {
    text-decoration: underline;
}
.toc-h1 { font-weight: bold; }
.toc-h2 { font-weight: 600; }
.toc-h3 { }
.toc-h4 { font-size: 0.9em; color: #666; }

/* 容器/提示块 */
.custom-container {
    padding: 15px 20px;
    border-radius: 6px;
    margin: 16px 0;
    border-left: 4px solid;
}
.custom-container .container-title {
    font-weight: bold;
    margin: 0 0 8px 0 !important;
    font-size: 1em;
}
.custom-container p:last-child {
    margin-bottom: 0;
}
.container-tip {
    background: #e8f5e9;
    border-color: #4caf50;
    color: #2e7d32;
}
.container-tip .container-title { color: #1b5e20; }

.container-info {
    background: #e3f2fd;
    border-color: #2196f3;
    color: #1565c0;
}
.container-info .container-title { color: #0d47a1; }

.container-warning {
    background: #fff8e1;
    border-color: #ff9800;
    color: #e65100;
}
.container-warning .container-title { color: #bf360c; }

.container-danger {
    background: #fce4ec;
    border-color: #f44336;
    color: #c62828;
}
.container-danger .container-title { color: #b71c1c; }

.container-note {
    background: #f3e5f5;
    border-color: #9c27b0;
    color: #6a1b9a;
}
.container-note .container-title { color: #4a148c; }

.container-success {
    background: #e8f5e9;
    border-color: #66bb6a;
    color: #2e7d32;
}
.container-success .container-title { color: #1b5e20; }

.container-details {
    background: #efebe9;
    border-color: #795548;
    color: #4e342e;
}
.container-details .container-title { color: #3e2723; }

.container-quote {
    background: #fafafa;
    border-color: #9e9e9e;
    color: #424242;
}
.container-quote .container-title { color: #212121; }

/* 上标、下标、高亮 */
sup { font-size: 0.75em; vertical-align: super; }
sub { font-size: 0.75em; vertical-align: sub; }
mark {
    background: #fff3a8;
    padding: 1px 4px;
    border-radius: 2px;
}

/* 数学公式块级样式 */
.math-block {
    margin: 1em 0;
    text-align: center;
    overflow-x: auto;
}

/* Mermaid 图表容器 */
.mermaid {
    margin: 1em 0;
    text-align: center;
}

/* Mermaid 图表缩放交互容器 v3.9.0 新增 */
/* 修改日期：2026-06-08 修改原因：支持 Mermaid 图表框内缩放/平移/还原 */
/* 修改日期：2026-06-08 修改原因：移除 border/background/margin，保持与原始渲染完全一致 */
/* 修改日期：2026-06-08 修改原因：margin 从 .mermaid 移至 wrapper，防止 margin 塌陷改变布局 */
.mermaid-zoom-wrapper {
    position: relative;
    overflow: hidden;
    margin: 1em 0;
    padding: 0;
    border: none;
    background: transparent;
    cursor: grab;
    touch-action: none;
}
.mermaid-zoom-wrapper:active {
    cursor: grabbing;
}
.mermaid-zoom-wrapper .mermaid {
    margin: 0;
    transform-origin: 0 0;
    transition: none;
    user-select: none;
    -webkit-user-select: none;
}
.mermaid-zoom-wrapper .mermaid.zoom-transition {
    transition: transform 0.3s ease;
}

/* Mermaid 还原按钮：常驻显示，使用 SVG 图标 */
.mermaid-zoom-reset-btn {
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 10;
    background: rgba(255, 255, 255, 0.85);
    border: 1px solid rgba(0, 0, 0, 0.15);
    border-radius: 4px;
    padding: 4px;
    cursor: pointer;
    color: #555;
    user-select: none;
    -webkit-user-select: none;
    line-height: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    width: 28px;
    height: 28px;
}
.mermaid-zoom-reset-btn:hover {
    background: rgba(255, 255, 255, 1);
    border-color: #999;
    color: #333;
}
.mermaid-zoom-reset-btn svg {
    width: 18px;
    height: 18px;
}

/* 脚注样式 v3.5.2 新增（MISS-2 修复） */
/* ParsedownExtra 生成 .footnotes 容器、.footnote-ref 引用、.footnote-backref 回链 */
/* 独立运行：不依赖主题，确保任何主题下脚注都有可读样式 */
.footnotes {
    margin-top: 2em;
    padding-top: 1em;
    border-top: 1px solid #e1e4e8;
    font-size: 0.9em;
    color: #586069;
}
.footnotes hr {
    display: none;
}
.footnotes ol {
    padding-left: 1.5em;
}
.footnotes li {
    margin-bottom: 0.4em;
    line-height: 1.6;
}
.footnote-ref {
    font-size: 0.85em;
    vertical-align: super;
    line-height: 0;
}
.footnote-ref a {
    color: #0366d6;
    text-decoration: none;
}
.footnote-ref a:hover {
    text-decoration: underline;
}
.footnote-backref {
    font-size: 0.85em;
    color: #0366d6;
    text-decoration: none;
    margin-left: 4px;
}
.footnote-backref:hover {
    text-decoration: underline;
}

/* 代码块暗色主题 v3.7.0 重构 */
/* 所有代码块（带标题/不带标题）统一使用暗色主题容器 */
/* Parser.php blockFencedCodeComplete() 生成 HTML 结构 */
/* 修改日期：2026-05-27 修改原因：替换浅色主题为暗色主题 */
.code-block-wrapper {
    position: relative;
    background-color: #1e1e1e;
    border-radius: 6px;
    padding: 0;
    margin: 20px 0;
    font-family: 'Consolas', 'Courier New', monospace;
}

.code-top-bar {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 6px 12px;
    background-color: #2d2d2d;
    border-radius: 6px 6px 0 0;
    user-select: none;
}

.code-title {
    color: #a9a9a9;
    font-size: 12px;
    font-weight: normal;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
    margin-right: 15px;
}

.lang-copy-btn {
    color: #858585;
    font-size: 12px;
    background: transparent;
    border: none;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 6px;
    transition: color 0.2s;
    flex-shrink: 0;
}

.lang-copy-btn:hover {
    color: #c4c4c4;
}

.lang-copy-btn .separator {
    margin: 0 2px;
}

.code-block-wrapper pre.actual-code-content {
    padding: 16px;
    margin: 0;
    color: #d4d4d4;
    overflow-x: auto;
    font-size: 14px;
    line-height: 1.5;
    background: transparent;
    border: none;
    border-radius: 0 0 6px 6px;
}
</style>
STYLE;
        echo $css;

        // 按需加载 KaTeX CSS：仅在页面包含数学公式时输出
        // CSS 必须在 <head> 中加载，否则页面会出现公式闪烁（FOUC）
        // @since v3.4.0 从 injectScripts() 迁移至此，增加按需检测
        // v3.8.0 修改：KaTeX CSS 路径改为通过 getResourceUrl() 动态获取，支持本地/CDN 切换
        // 修改日期：2026-05-27 修改原因：增强按需加载，支持 CDN 资源来源选择
        $baseUrl = '/' . self::ASSETS_SUBDIR;
        if (self::$hasMath) {
            $katexCssUrl = self::getResourceUrl('katexSource', 'katexCdnBase', '/katex/katex.min.css', 'katex.min.css');
            echo '<link rel="stylesheet" href="' . $katexCssUrl . '">' . "\n";
        }

        // 按需加载 Prism.js CSS：仅在页面包含代码块时输出
        // CSS 必须在 <head> 中加载，避免代码块出现无样式闪烁（FOUC）
        // @since v3.6.0 新增
        if (self::$hasCodeBlock) {
            echo '<link rel="stylesheet" href="' . $baseUrl . '/prism/prism.css">' . "\n";
        }
    }

    /**
     * 注入 KaTeX、Mermaid 和 Prism.js 前端渲染脚本
     *
     * 挂载到文章页面底部（footer hook），按配置按需加载：
     * - KaTeX：JS + auto-render 自动扫描公式标签并渲染
     * - Mermaid：JS + 初始化脚本自动扫描 .mermaid 元素并渲染
     * - Prism.js：JS 自动扫描代码块并应用语法高亮
     *
     * 资源文件从插件本地目录加载，不依赖外部 CDN。
     *
     * @static
     * @access public
     * @param string|null $footer 页面 footer 内容
     * @param \Widget\Archive|null $archive 当前文章归档对象
     * @return void
     * @since v3.3.0 新增
     */
    public static function injectScripts($footer = null, $archive = null)
    {
        $baseUrl = '/' . self::ASSETS_SUBDIR;

        // KaTeX 数学公式渲染（仅在实际包含公式元素时加载）
        // parse() 已检测页面内容并设置 self::$hasMath 标志
        // KaTeX CSS 已在 injectStyles() 中按需输出到 <head>，此处仅加载 JS
        // @since v3.4.0 改为基于内容的按需加载，避免无公式页面加载 ~1.5MB 资源
        // v3.8.0 修改：KaTeX JS 路径改为通过 getResourceUrl() 动态获取，支持本地/CDN 切换
        // 修改日期：2026-05-27 修改原因：增强按需加载，支持 CDN 资源来源选择
        if (self::$hasMath) {
            $katexJsUrl = self::getResourceUrl('katexSource', 'katexCdnBase', '/katex/katex.min.js', 'katex.min.js');
            // 修正日期：2026-05-27 修正原因：KaTeX 0.17.0 CDN 中 auto-render.min.js 位于 dist/contrib/ 子目录
            $autoRenderUrl = self::getResourceUrl('katexSource', 'katexCdnBase', '/katex/auto-render.min.js', 'contrib/auto-render.min.js');

            // KaTeX 核心渲染引擎
            echo '<script defer src="' . $katexJsUrl . '"></script>' . "\n";

            // KaTeX auto-render：自动扫描页面中的数学公式标签并渲染
            echo '<script defer src="' . $autoRenderUrl . '"'
                . ' onload="renderMathInElement(document.body, {'
                . 'delimiters: ['
                . '{left: \'\\\\(\', right: \'\\\\)\', display: false},'
                . '{left: \'\\\\[\', right: \'\\\\]\', display: true},'
                . '{left: \'$\', right: \'$\', display: false},'
                . '{left: \'$$\', right: \'$$\', display: true}'
                . '],'
                . 'ignoredTags: [\'script\', \'noscript\', \'style\', \'textarea\', \'pre\', \'code\', \'option\']'
                . '});"'
                . '></script>' . "\n";
        }

        // Mermaid 图表渲染（仅在实际包含图表元素时加载）
        // parse() 已检测页面内容并设置 self::$hasMermaid 标志
        // @since v3.4.0 改为基于内容的按需加载，避免无图表页面加载 3.2MB 资源
        // v3.8.0 修改：Mermaid JS 路径改为通过 getResourceUrl() 动态获取，支持本地/CDN 切换
        // 修改日期：2026-05-27 修改原因：增强按需加载，支持 CDN 资源来源选择
        if (self::$hasMermaid) {
            $mermaidJsUrl = self::getResourceUrl('mermaidSource', 'mermaidCdnBase', '/mermaid.min.js', 'mermaid.min.js');
            echo '<script src="' . $mermaidJsUrl . '"></script>' . "\n";
            echo '<script>mermaid.initialize({startOnLoad:true,theme:\'default\'});</script>' . "\n";

            // Mermaid 图表缩放/平移/还原交互脚本
            // 修改日期：2026-06-08 修改原因：v3.9.0 新增图表框内缩放交互功能
            // 功能：鼠标滚轮缩放（跟随光标）、拖拽平移、右上角还原按钮
            // 兼容本地模式和 CDN 模式，通过 MutationObserver 异步监听渲染完成
            echo '<script>'
                . '(function(){'
                // wrapMermaid：为已渲染的 .mermaid 元素创建缩放交互容器
                // 仅包裹未处理过的元素（通过 .mermaid-zoom-wrapper 类名去重）
                . 'function wrapMermaid(el){'
                . 'if(el.closest(\'.mermaid-zoom-wrapper\'))return;'
                . 'var w=document.createElement(\'div\');'
                . 'w.className=\'mermaid-zoom-wrapper\';'
                . 'var b=document.createElement(\'button\');'
                . 'b.className=\'mermaid-zoom-reset-btn\';'
                . 'b.innerHTML=\'<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0 3.181 3.183a8.25 8.25 0 0 0 13.803-3.7M4.031 9.865a8.25 8.25 0 0 1 13.803-3.7l3.181 3.182m0-4.991v4.99"></path></svg>\';'
                . 'w.appendChild(b);'
                . 'el.parentNode.insertBefore(w,el);'
                . 'w.appendChild(el);'
                // 状态：当前缩放比例和平移偏移
                . 'var s=1,tx=0,ty=0,initH=0;'
                // updateTransform：应用 transform，wrapper 高度固定不变（保持原始大小）
                // 修改日期：2026-06-08 修改原因：移除动态高度调整，防止框大小随缩放变化
                . 'function updateTransform(){'
                . 'el.style.transform=\'translate(\'+tx+\'px,\'+ty+\'px) scale(\'+s+\')\';'
                . '}'
                // getLocalPos：获取鼠标/触摸点相对于容器的坐标
                . 'function getLocalPos(e){'
                . 'var rect=w.getBoundingClientRect();'
                . 'var cx=e.touches?e.touches[0].clientX:e.clientX;'
                . 'var cy=e.touches?e.touches[0].clientY:e.clientY;'
                . 'return{x:cx-rect.left,y:cy-rect.top};'
                . '}'
                // --- 滚轮缩放（鼠标光标跟随） ---
                . 'w.addEventListener(\'wheel\',function(e){'
                . 'e.preventDefault();'
                . 'var p=getLocalPos(e);'
                // deltaY > 0 为向下滚（缩小），< 0 为向上滚（放大）
                . 'var f=e.deltaY>0?0.9:1.1;'
                . 'var ns=s*f;'
                // 限制缩放范围：0.05 ~ 20（基本无限制）
                . 'if(ns<0.05||ns>20)return;'
                // 缩放中心跟随鼠标：保持鼠标下的内容点在缩放前后位置不变
                . 'tx=p.x*(1-f)+tx*f;'
                . 'ty=p.y*(1-f)+ty*f;'
                . 's=ns;'
                . 'updateTransform();'
                . '},{passive:false});'
                // --- 鼠标拖拽平移 ---
                . 'var dragging=false,dx=0,dy=0;'
                . 'w.addEventListener(\'mousedown\',function(e){'
                . 'if(e.target===b)return;'
                . 'dragging=true;dx=e.clientX;dy=e.clientY;'
                . '});'
                . 'document.addEventListener(\'mousemove\',function(e){'
                . 'if(!dragging)return;'
                // 拖拽直接 1:1 跟随鼠标移动，不除以缩放比例
                . 'tx+=(e.clientX-dx);'
                . 'ty+=(e.clientY-dy);'
                . 'dx=e.clientX;dy=e.clientY;'
                . 'updateTransform();'
                . '});'
                . 'document.addEventListener(\'mouseup\',function(){dragging=false;});'
                // --- 触摸平移（单指拖拽） ---
                . 'w.addEventListener(\'touchstart\',function(e){'
                . 'if(e.touches.length===1&&!pinching){'
                . 'dx=e.touches[0].clientX;dy=e.touches[0].clientY;'
                . '}'
                . '},{passive:true});'
                . 'w.addEventListener(\'touchmove\',function(e){'
                . 'if(e.touches.length===1&&!pinching){'
                . 'e.preventDefault();'
                . 'tx+=(e.touches[0].clientX-dx);'
                . 'ty+=(e.touches[0].clientY-dy);'
                . 'dx=e.touches[0].clientX;dy=e.touches[0].clientY;'
                . 'updateTransform();'
                . '}'
                . '},{passive:false});'
                // --- 双指捏合缩放（移动端） ---
                . 'var lastPinchDist=0,pinching=false,pinchMid={x:0,y:0};'
                . 'w.addEventListener(\'touchstart\',function(e){'
                . 'if(e.touches.length===2){'
                . 'pinching=true;'
                // 计算两指中点作为缩放中心
                . 'pinchMid={x:(e.touches[0].clientX+e.touches[1].clientX)/2,y:(e.touches[0].clientY+e.touches[1].clientY)/2};'
                . 'lastPinchDist=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);'
                . '}'
                . '},{passive:true});'
                . 'w.addEventListener(\'touchmove\',function(e){'
                . 'if(e.touches.length===2&&pinching){'
                . 'e.preventDefault();'
                . 'var d=Math.hypot(e.touches[0].clientX-e.touches[1].clientX,e.touches[0].clientY-e.touches[1].clientY);'
                . 'if(lastPinchDist>0){'
                . 'var f=d/lastPinchDist;'
                . 'var ns=s*f;'
                . 'if(ns>=0.05&&ns<=20){'
                // 缩放中心跟随两指中点
                . 'var rect=w.getBoundingClientRect();'
                . 'var mx=pinchMid.x-rect.left,my=pinchMid.y-rect.top;'
                . 'tx=mx*(1-f)+tx*f;'
                . 'ty=my*(1-f)+ty*f;'
                . 's=ns;'
                . 'updateTransform();'
                . '}'
                . '}'
                . 'lastPinchDist=d;'
                . 'pinchMid={x:(e.touches[0].clientX+e.touches[1].clientX)/2,y:(e.touches[0].clientY+e.touches[1].clientY)/2};'
                . '}'
                . '},{passive:false});'
                . 'w.addEventListener(\'touchend\',function(){pinching=false;lastPinchDist=0;},{passive:true});'
                // --- 还原按钮：平滑过渡回初始状态 ---
                . 'b.addEventListener(\'click\',function(e){'
                . 'e.stopPropagation();'
                . 's=1;tx=0;ty=0;'
                . 'el.classList.add(\'zoom-transition\');'
                . 'updateTransform();'
                . 'setTimeout(function(){el.classList.remove(\'zoom-transition\');},300);'
                . '});'
                // 初始高度同步：记录 mermaid 原始高度并固定到 wrapper（仅执行一次）
                // 修改日期：2026-06-08 修改原因：wrapper 高度固定，不随缩放变化
                . 'setTimeout(function(){'
                . 'initH=el.getBoundingClientRect().height;'
                . 'w.style.height=initH+\'px\';'
                . '},100);'
                . '}'
                // initZoom：扫描页面中已渲染完成的 .mermaid 元素并包裹
                . 'function initZoom(){'
                . 'document.querySelectorAll(\'.mermaid[data-processed]\').forEach(function(el){'
                . 'if(!el.closest(\'.mermaid-zoom-wrapper\'))wrapMermaid(el);'
                . '});'
                . '}'
                // 首次执行：处理页面加载时已渲染的图表
                . 'initZoom();'
                // MutationObserver：持续监听 DOM 变化，处理异步渲染的图表
                // 兼容 CDN 模式（Mermaid 加载可能较慢，渲染延迟）
                . 'var obs=new MutationObserver(function(muts){'
                . 'muts.forEach(function(m){'
                . 'm.addedNodes.forEach(function(n){'
                . 'if(n.nodeType===1){'
                . 'if(n.classList&&n.classList.contains(\'mermaid\')&&n.hasAttribute(\'data-processed\')){'
                . 'if(!n.closest(\'.mermaid-zoom-wrapper\'))wrapMermaid(n);'
                . '}'
                // 检查子节点（Mermaid 可能插入已存在的容器中）
                . 'var els=n.querySelectorAll?n.querySelectorAll(\'.mermaid[data-processed]\'):[];'
                . 'els.forEach(function(el){'
                . 'if(!el.closest(\'.mermaid-zoom-wrapper\'))wrapMermaid(el);'
                . '});'
                . '}'
                . '});'
                . '});'
                . '});'
                . 'obs.observe(document.body,{childList:true,subtree:true});'
                // 兜底：3 秒后再次扫描，防止 MutationObserver 遗漏
                . 'setTimeout(initZoom,3000);'
                . '})();'
                . '</script>' . "\n";
        }

        // Prism.js 代码语法高亮（仅在实际包含代码块时加载）
        // parse() 已检测页面内容并设置 self::$hasCodeBlock 标志
        // Prism.js 自动扫描页面中的 <code class="lang-xxx"> 元素并应用语法着色
        // @since v3.6.0 新增
        if (self::$hasCodeBlock) {
            echo '<script src="' . $baseUrl . '/prism/prism.js"></script>' . "\n";

            // 代码块复制按钮功能
            // 点击复制按钮 → 提取 <code> 内容 → 写入剪贴板 → 显示"已复制"反馈
            // @since v3.7.0 新增：暗色主题重构，复制按钮由 Parser.php 生成
            echo '<script>'
                . 'function copyCode(button){'
                . 'var wrapper=button.closest(\'.code-block-wrapper\');'
                . 'var codeElement=wrapper.querySelector(\'code\');'
                . 'var copyTextSpan=button.querySelector(\'.copy-text\');'
                . 'navigator.clipboard.writeText(codeElement.innerText).then(function(){'
                . 'var originalText=copyTextSpan.innerText;'
                . 'copyTextSpan.innerText=\'已复制\';'
                . 'setTimeout(function(){copyTextSpan.innerText=originalText},2000)'
                . '})}'
                . '</script>' . "\n";
        }
    }
}
