[PHP]轻量主题函数WordPress 集成 KaTeX 数学公式渲染

发布者: 站长-R 分类: PHP,web服务建站,计算机大类 发布时间: 2026-09-11 18:45 访问量: 98 次浏览

WordPress 集成 KaTeX 数学公式渲染完整方案

一、为什么选 KaTeX

写数学博客绕不开公式渲染这个问题。WordPress 原生不支持 LaTeX,必须借助第三方库。主流选择有两个:MathJax 和 KaTeX。虽然已有许多插件能够实现,但是大多数需要用短代码形式渲染,短代码方案体验略差,我们重新实现直接渲染的方式实现。

MathJax 功能全面,LaTeX 支持最广,但体积大、渲染慢。它是异步渲染,页面加载时公式会先以原始代码形式出现,过一会儿才替换成排版结果,公式一多就会有明显的闪烁感。KaTeX 是 Khan Academy 开源的方案,采用同步渲染,公式几乎在页面加载完成的同时就出现,体积也小得多。代价是 LaTeX 支持范围不如 MathJax 全,一些冷门宏包无法使用,但对于数学博客的日常写作来说完全够用。

二、两种部署方案

KaTeX 就最重要的三个东西:katex.min.csskatex.min.jsfonts/ 文件夹,可以选择利用CDN引入,也可以选择本地引入。

本地部署是把文件下载下来放到自己的服务器上,通过主题目录加载。优点是不依赖第三方服务,CDN 出问题公式照样显示;同域名加载速度快;版本完全可控。缺点是初次部署需要手动下载和上传,占用两三 MB 服务器空间。这是推荐方案。

CDN 加载是直接从 jsDelivr 加载 KaTeX 文件,复制代码就能用,适合快速测试。缺点是国内访问 jsDelivr 时快时慢,偶尔会超时导致公式不显示,长期使用还是本地部署更稳定。

两种方案只有第一步不同,后面的代码完全一样。

三、方案 A:本地部署

3.1 下载并上传

打开 KaTeX 的 GitHub Releases 页面(https://github.com/KaTeX/KaTeX/releases),下载最新版本,解压后会得到一个 katex 文件夹,里面包含 katex.min.csskatex.min.jscontrib/ 子文件夹和 fonts/ 子文件夹。
在这里插入图片描述

需要特别注意的是 fonts 文件夹里的 .woff2 字体文件一个都不能少。katex.min.css 通过相对路径 fonts/KaTeX_xxx.woff2 引用它们,缺了字体公式里的符号会显示成方框或乱码。

用 FTP 或主机控制面板的文件管理器,把整个 katex 文件夹上传到 wp-content/themes/你的主题/katex/。如果用的是子主题,就放在子主题目录下,本文后面的代码用 get_stylesheet_directory_uri() 获取路径,会自动指向当前激活的主题。

3.2 完整代码

把以下代码加到主题的 functions.php 末尾:

<?php
/**
 * 自定义 KaTeX 渲染(本地资源 + 直接渲染 + 精确限定范围)
 * 支持 $$...$$(块级)和 $...$(行内)
 * 保护代码块和行内代码不被公式提取干扰
 */

// ========== 1. 加载本地 KaTeX 资源 ==========
function custom_katex_assets() {
    if ( ! is_singular() ) return;
    $katex_url = get_stylesheet_directory_uri() . '/katex/';
    wp_enqueue_style( 'katex-style', $katex_url . 'katex.min.css', array(), '0.16.22' );
    wp_enqueue_script( 'katex-core', $katex_url . 'katex.min.js', array(), '0.16.22', true );
}
add_action( 'wp_enqueue_scripts', 'custom_katex_assets', 99 );

// ========== 2. 提取公式并替换为占位 span ==========
function custom_extract_katex( $content ) {
    if ( ! is_singular() || ! in_the_loop() || ! is_main_query() ) {
        return $content;
    }

    // 存放被保护的代码片段
    $placeholders = array();
    $i = 0;

    // 保护函数:用 HTML 注释做占位符
    $protect = function( $m ) use ( &$placeholders, &$i ) {
        $key = '<!--KATEX_CODE_' . $i . '-->';
        $placeholders[ $key ] = $m[0];
        $i++;
        return $key;
    };

    // 1) 保护 Markdown 代码块 
    $content = preg_replace_callback( '//s', $protect, $content );

    // 2) 保护 Markdown 行内代码 
    $content = preg_replace_callback( '/\n]+`/', $protect, $content );

    // 3) 保护已有的 <pre>...</pre> 标签
    $content = preg_replace_callback( '/<pre\b[^>]*>.*?<\/pre>/is', $protect, $content );

    // 4) 保护已有的 <code>...</code> 标签
    $content = preg_replace_callback( '/<code\b[^>]*>.*?<\/code>/is', $protect, $content );

    // 5) 保护已有的 <script>...</script>、<style>...</style>
    $content = preg_replace_callback( '/<script\b[^>]*>.*?<\/script>/is', $protect, $content );
    $content = preg_replace_callback( '/<style\b[^>]*>.*?<\/style>/is', $protect, $content );

    // 公式准备函数
    $prepare = function( $raw ) {
        // 还原 Markdown 产生的 <em> 斜体标签为下划线
        $raw = str_replace( array( '<em>', '</em>' ), '_', $raw );
        // 解码 HTML 实体,再编码,避免破坏 HTML 结构
        $formula = html_entity_decode( trim( $raw ), ENT_QUOTES | ENT_HTML5, 'UTF-8' );
        return htmlspecialchars( $formula, ENT_QUOTES, 'UTF-8' );
    };

    // 提取块级公式 $$...$$
    $content = preg_replace_callback(
        '/\$\$(.+?)\$\$/s',
        function ( $m ) use ( $prepare ) {
            return '<span class="katex-eq" data-display="true">' . $prepare( $m[1] ) . '</span>';
        },
        $content
    );

    // 提取行内公式 $...$
    $content = preg_replace_callback(
        '/(?<!\$)\$(?!\$)(.+?)(?<!\$)\$(?!\$)/s',
        function ( $m ) use ( $prepare ) {
            return '<span class="katex-eq" data-display="false">' . $prepare( $m[1] ) . '</span>';
        },
        $content
    );

    // 恢复被保护的代码片段
    foreach ( $placeholders as $key => $value ) {
        $content = str_replace( $key, $value, $content );
    }

    return $content;
}
add_filter( 'the_content', 'custom_extract_katex', -999 );

// ========== 3. 在文章外层包裹唯一容器 ==========
function custom_wrap_katex_content( $content ) {
    if ( is_singular() && in_the_loop() && is_main_query() ) {
        return '<div id="katex-content">' . $content . '</div>';
    }
    return $content;
}
add_filter( 'the_content', 'custom_wrap_katex_content', 1 );

// ========== 4. 页脚渲染脚本 ==========
function custom_katex_footer_script() {
    if ( ! is_singular() ) return;
    ?>
    <script>
    document.addEventListener('DOMContentLoaded', function () {
        var container = document.getElementById('katex-content');
        if (!container) return;

        var elements = container.querySelectorAll('.katex-eq');
        Array.prototype.forEach.call(elements, function (el) {
            var formula = el.textContent;
            var display = el.getAttribute('data-display') === 'true';
            var span = document.createElement('span');
            try {
                katex.render(formula, span, {
                    displayMode: display,
                    throwOnError: false
                });
            } catch (e) {
                span.style.color = 'red';
                span.textContent = e.message;
            }
            el.parentNode.replaceChild(span, el);
        });
    });
    </script>
    <?php
}
add_action( 'wp_footer', 'custom_katex_footer_script', 100 );
?>

四、方案 B:CDN 加载

如果不想下载文件,把上面第一段 custom_katex_assets 替换成:

function custom_katex_assets() {
    if ( ! is_singular() ) return;
    wp_enqueue_style(
        'katex-style',
        'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.css',
        array(), '0.16.22'
    );
    wp_enqueue_script(
        'katex-core',
        'https://cdn.jsdelivr.net/npm/katex@0.16.22/dist/katex.min.js',
        array(), '0.16.22', true
    );
}
add_action( 'wp_enqueue_scripts', 'custom_katex_assets', 99 );

其余代码不变。

五、实现原理

5.1 第一段:加载资源

这一段负责把 KaTeX 的 CSS 和 JS 加载到文章页面。is_singular() 判断当前是不是文章页或单页面,如果不是就直接返回,不在首页、归档页、搜索结果页浪费带宽。wp_enqueue_style()wp_enqueue_script() 是 WordPress 标准的资源加载函数,把文件注册到资源队列里由 WordPress 统一输出。最后一个参数 true 表示把脚本放在 </body> 前而不是 <head> 里,这样 HTML 先解析、脚本后加载,不会阻塞页面渲染。

5.2 第二段:提取公式

这是整个方案的核心。优先级设成 -999,让它在 Markdown 解析器之前运行,把公式从文本里提取出来替换成 <span> 标签。这样 Markdown 再怎么处理也动不了公式内容。并且过滤掉无需渲染的部分,即代码块和引用块。

$prepare 闭包里做两件事。第一件是把 <em></em> 还原成 _,解决下划线被 Markdown 转成斜体标签的问题。第二件是做一次 HTML 实体的解码和编码,把 < 还原成 <,让 KaTeX 拿到正确的 LaTeX 源码,同时保证公式内容不会破坏 HTML 结构。

处理顺序是先块级 $$...$$ 再行内 $...$,避免 $$ 被行内正则从中间切开匹配成两个错误的行内公式。

5.3 第三段:包裹容器

给文章正文包一个 <div id="katex-content">。页脚脚本只在这个容器里找公式,不会误渲染标题、侧边栏、热门文章里的 $ 符号。is_singular()in_the_loop()is_main_query() 三个条件确保只在文章正文上包裹,不影响其他调用 the_content 的地方。

5.4 第四段:页脚渲染

在页脚遍历所有 .katex-eq 占位 span。用 el.textContent 读取公式内容,浏览器会自动把 < 解码成 <,所以 KaTeX 拿到的是正确的 LaTeX 源码。用 data-display 区分块级和行内,调用 katex.render() 渲染,然后替换掉占位 span。throwOnError: false 让公式出错时显示红色错误信息,而不是中断整个页面。

六、公式写作规范

由于这个函数规避了一些WordPress的Markdown字符过滤,所以需要注意一些不同的Katex的符号。

6.1 定界符

行内公式用 $...$,例如 $x_n$$\sin x$。块级公式用 $$...$$,例如 $$ \left[-\frac{\pi}{2},\frac{\pi}{2}\right] $$

不要用 \(...\)\[...\]。Markdown 会把里面的反斜杠当作转义字符吃掉,KaTeX 看到的是裸括号,不会渲染。$ 不是 Markdown 的转义字符,所以安全。

6.2 花括号

凡是需要显示为花括号的地方,都用 \lbrace\rbrace。具体来说,\{ 改成 \lbrace\} 改成 \rbrace\Big\{ 改成 \Big\lbrace\Big\} 改成 \Big\rbrace

原因和定界符一样。\{\} 里的反斜杠会被 Markdown 吃掉,变成裸花括号。KaTeX 看到裸花括号会报语法错误,因为在 LaTeX 里花括号是分组符号,不是可视定界符。\lbrace\rbrace 里的 \l\r 不是 Markdown 转义序列,安全。

6.3 块级公式格式

块级公式独立成行,前后空行,公式内容写在一行内不换行,不用加 \displaystyle。独立成行是因为 Markdown 会把连续的行合并成段落,混在一行会让公式的 display 属性失效。一行写完是因为多行公式容易被解析成列表或代码块,导致 $$ 被拆散。

6.4 其他字符

百分号用 \%,因为 % 在 LaTeX 里是注释符号。井号用 \#,因为 # 在 LaTeX 里是宏参数。下划线 _ 直接写,不用 \_,KaTeX 支持。

6.5 示例

错误写法:

设 \(\{x_n\}\) 是数列,若 \(\lim_{n\to\infty} x_n = A\)
\[
\delta=\min\Big\{ f(x_0+\varepsilon)-y_0,\ y_0-f(x_0-\varepsilon) \Big\}>0.
\]

正确写法:

设 $\lbrace x_n \rbrace$ 是数列,若 $\lim_{n\to\infty} x_n = A$

$$ \delta=\min\Big\lbrace f(x_0+\varepsilon)-y_0,\ y_0-f(x_0-\varepsilon) \Big\rbrace>0. $$

八、总结

核心思路就是,在 Markdown 解析之前把公式提取到 HTML 标签里,解析之后再用 JS 精确渲染,这个函数直接放在主题的function.php就可以实现。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注