LaTeX 错误渲染了 PHP 代码,wordpress插件解决方案

1. 问题描述

在自己的 wordpress 网站记录一篇 PHP 相关的文章时,遇到了一个网页的渲染问题:PHP 的变量符号 $ 与 markdown 插件的 LaTeX 公式渲染标识符 $....$ 冲突了,这导致正文以及代码块中的一些 PHP 函数被错误解析为数学公式。

例如:PHP 函数 function update( $new_instance, $old_instance ) 在正文中被错误解析为 function update( new_instance,old_instance )。

这个问题在大部分支持良好的 markdown 解释器下都不是很严重的问题,但是由于 wordpress 并不原生支持 markdown, 我是通过插件 WP Editor.md 实现的 markdown 支持, 而这个插件的 LaTeX 识别会导致这个现象(github 上也有人反应相关 issue)。
由于这个插件已经是比较好的支持中文的 markdown 插件了,所以并不准备更换, 最后从下文的三种解决方法中选择了第一种方式。

2. Simple Mathjax 插件解决

我们可以禁用 WP Editor.md 的 LaTeX 渲染功能,诉诸于别的更专长的数学公式插件。
我选择的是 Simple MathJax
其原理就是帮助你在生成的 HTML 文件中调取 MathJax 库,并且通过特定的标识符号检测出需要渲染的 LaTeX 代码,最终生成可以被浏览器渲染的 HTML 文本。

MathJax 是一个用于在网页中显示数学公式的 JavaScript 库

然而禁用 WP Editor.md 的 LaTeX 功能,直接更换并启用 Simple MathJax 插件后,虽然代码块内的 PHP 代码可以被正常规避公式检测成功以代码形式渲染了,正文中形如 function update( $new_instance, $old_instance ) 的函数仍然会被错误识别的数学公式。我们需要进一步的修改 Simple MathJax 插件。

2.1 修改 Simple MathJax 公式检测代码

进入插件修改页面
通过下图红框中的入口,进入插件的编辑器页面,在这里我们就可以修改插件的源码了。
注意:在修改插件的代码前,最好先禁用相关插件以防出现无法预测的错误。

 

SimpleMathJax 公式检测逻辑
插件原始采用的是最经典的检测方案,既同时允许两种公式标识符:

  1. $....$$$....$$
    最常见的 LaTeX 的 $ 系标识符号:以 $....$ 标识行内公式,以 $$....$$ 标识行外公式。
  2. \(....\), \[....\]
    更加不容易冲突的 \( 系标识符号:以 \(....\) 标识行内公式,以 \[....\] 标识行外公式。

显然第一种检测方案就是渲染冲突的问题所在,我们需要在插件源代码中将其取消掉,只使用不容易冲突的 \( 系标识符号。
 

代码修改
通过阅读我们发现插件显式的定义了行内公式 inlineMath 属性,定义部分的代码如下:

 /*
  * Default MathJax configuration scripts, for each major version.
  */
 public static $default_configs = array(
   2 => "MathJax.Hub.Config({\n  tex2jax: {\n    inlineMath: [['$','$'], ['\\\\(','\\\\)']],\n    processEscapes: true,\n    ignoreHtmlClass: 'tex2jax_ignore|editor-rich-text'\n  }\n});\n",
   3 => "MathJax = {\n  tex: {\n    inlineMath: [['$','$'],['\\\\(','\\\\)']], \n    processEscapes: true\n  },\n  options: {\n    ignoreHtmlClass: 'tex2jax_ignore|editor-rich-text'\n  }\n};\n",
   4 => "MathJax = {\n  tex: {\n    inlineMath: [['$','$'],['\\\\(','\\\\)']], \n    processEscapes: true\n  },\n  options: {\n    ignoreHtmlClass: 'tex2jax_ignore|editor-rich-text'\n  }\n};\n"
 );

其中 ['$','$'] 代表以 $....$ 标识行内公式,['\\\\(','\\\\)'] 代表以 \(....\) 标识行内公式。
 

为什么是 \\\\( 这里有两层转义:

  1. 在 PHP 字符串中,一个反斜杠 \ 是转义符。为了在字符串里得到一个字面上的 \,你需要写成 \\。所以 \\\\( 在 PHP 内存中变成了字符串 \\ (
  2. 当这个字符串 \\ ( 被输出到前端并由 JavaScript 读取时,JavaScript 也视 \ 为转义符。它会将 \\ 解析为字面上的 \,将 \\( 解析为字面上的 \(

最终效果:在 MathJax 实际工作时,它收到的定界符就是 \(\)
 

于是为了最终解决 $ 系标识符号行内公式标识符导致的渲染混乱,我们吧代码中的相关部分删除并更新插件即可,最终形如:

 /*
  * Default MathJax configuration scripts, for each major version.
  */
 public static $default_configs = array(
   2 => "MathJax.Hub.Config({\n  tex2jax: {\n    inlineMath: [['\\\\(','\\\\)']],\n    processEscapes: true,\n    ignoreHtmlClass: 'tex2jax_ignore|editor-rich-text'\n  }\n});\n",
   3 => "MathJax = {\n  tex: {\n    inlineMath: [['\\\\(','\\\\)']], \n    processEscapes: true\n  },\n  options: {\n    ignoreHtmlClass: 'tex2jax_ignore|editor-rich-text'\n  }\n};\n",
   4 => "MathJax = {\n  tex: {\n    inlineMath: [['\\\\(','\\\\)']], \n    processEscapes: true\n  },\n  options: {\n    ignoreHtmlClass: 'tex2jax_ignore|editor-rich-text'\n  }\n};\n"
 );

注意:我们只修改了行内公式的 $....$ 标识符,行外标识符 $$....$$ 仍然有效。

2.2 Simple MathJax 使用例

*行内公式:*
\\(K_k = P_{k|k-1} H_k^T (H_k P_{k|k-1} H_k^T + R_k)^{-1}\\)
*行外公式:*
\\[K_k = P_{k|k-1} H_k^T (H_k P_{k|k-1} H_k^T + R_k)^{-1}\\]

显示效果

行内公式:
K_k = P_{k|k-1} H_k^T (H_k P_{k|k-1} H_k^T + R_k)^{-1}
行外公式:
K_k = P_{k|k-1} H_k^T (H_k P_{k|k-1} H_k^T + R_k)^{-1}

3. 内嵌 HTML 代码解决

禁用 WP Editor.md 的 LaTeX 渲染功能后,如果也不想引入更多的插件的话,还有一种适合最多平台的情况,那就是直接使用 HTML 文本来进行公式渲染。
 
可查阅并参考文档MathML | MDN

<!-- 在 head 内部引用需要的解释脚本可以使得渲染更加漂亮 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.2/MathJax.js?config=TeX-MML-AM_CHTML"></script>
<!-- 不引用也可以直接输出有字体的公式 -->
<link rel="stylesheet" href="https://fred-wang.github.io/MathFonts/LatinModern/mathfonts.css" />
<math xmlns="http://www.w3.org/1998/Math/MathML"><msub><mi>K</mi><mi>k</mi></msub><mo>=</mo><msub><mi>P</mi><mrow><mi>k</mi><mrow><mo stretchy="false">|</mo></mrow><mi>k</mi><mo>−</mo><mn>1</mn></mrow></msub><msubsup><mi>H</mi><mi>k</mi><mi>T</mi></msubsup><mo stretchy="false">(</mo><msub><mi>H</mi><mi>k</mi></msub><msub><mi>P</mi><mrow><mi>k</mi><mrow><mo stretchy="false">|</mo></mrow><mi>k</mi><mo>−</mo><mn>1</mn></mrow></msub><msubsup><mi>H</mi><mi>k</mi><mi>T</mi></msubsup><mo>+</mo><msub><mi>R</mi><mi>k</mi></msub><msup><mo stretchy="false">)</mo><mrow><mo>−</mo><mn>1</mn></mrow></msup></math>
<link rel="stylesheet" href="https://fred-wang.github.io/MathFonts/LatinModern/mathfonts.css" />
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"><msub><mi>K</mi><mi>k</mi></msub><mo>=</mo><msub><mi>P</mi><mrow><mi>k</mi><mrow><mo stretchy="false">|</mo></mrow><mi>k</mi><mo>−</mo><mn>1</mn></mrow></msub><msubsup><mi>H</mi><mi>k</mi><mi>T</mi></msubsup><mo stretchy="false">(</mo><msub><mi>H</mi><mi>k</mi></msub><msub><mi>P</mi><mrow><mi>k</mi><mrow><mo stretchy="false">|</mo></mrow><mi>k</mi><mo>−</mo><mn>1</mn></mrow></msub><msubsup><mi>H</mi><mi>k</mi><mi>T</mi></msubsup><mo>+</mo><msub><mi>R</mi><mi>k</mi></msub><msup><mo stretchy="false">)</mo><mrow><mo>−</mo><mn>1</mn></mrow></msup></math>

 

显示效果

Kk=Pk|k1HkT(HkPk|k1HkT+Rk)1 Kk=Pk|k1HkT(HkPk|k1HkT+Rk)1

4. 修改 WP Editor.md 源代码

我们也可以完整的修改插件的相关代码,从而更优雅的解决这个问题。
 
首先定位插件和 LaTeX 渲染相关的代码位置在 wp-editormd/src/App/KaTeX.php
阅读原始代码后发现,负责检测公式标识符的函数 katex_markup_single() katex_markup_double(),都允许 $ 系标识符和内部 LaTeX 代码之间存在空格,即:

$y=f(x)$$y=f(x) $ 都可以被正确解析为 y=f(x)
$$....$$ 的情况相同

而很多 LaTeX 渲染脚本都不允许这种情况存在,也就避免了上文中形如函数 function update( $new_instance, $old_instance ) 被错误解析为 function update( new_instance,old_instance ) 的情况。
 
于是我们仅仅需要为源代码加上
不允许标识符与 LaTeX 公式代码之间存在空格
的逻辑即可。
 
更新后的 KaTeX.php 已上传至 Github: https://github.com/lichenrobo/WP-Editor.md/

4.1 代码解析

其中最核心的就是一个统一的正则表达式,分析如下:

/**
 * 一个用于捕获行外和行内数学公式的统一正则表达式。
 * 使用 OR '|' 操作符,并优先尝试匹配更长的 '$$' 标识符。
 * 这可以防止 '$$' 被错误地解析为两个独立的 '$' 。
 *
 * 正则表达式分解:
 * 捕获组 1: 匹配整个 $$...$$ 块。
 *  ->捕获组 2: 捕获 $$...$$ 内部的实际 LaTeX 内容。
 * 捕获组 3: 匹配整个 $...$ 块。
 *  ->捕获组 4: 捕获 $...$ 内部的实际 LaTeX 内容。
 */
$regex = '/
    (                                      # 捕获组 1: 捕获 $$...$$ 块
        \$\$                               # 以 $$ 开始
        (                                  # 捕获组 2: 捕获内容
            (?:
                [^$]+                        # 匹配任何非美元符号的字符
                |                          # 或
                \$ (?<! \$\$ )               # 匹配一个美元符号,前提是它前面不是另一个美元符号
            )+?                            # 一次或多次非贪婪匹配
        )
        \$\$                               # 以 $$ 结束
    )
    |                                      # 或
    (                                      # 捕获组 3: 捕获 $...$ 块
        \$                                 # 以 $ 开始
        (                                  # 捕获组 4: 捕获内容
            (?:
                [^$]+                        # 匹配任何非美元符号的字符
                |                          # 或
                \$ (?<! \$\$ )               # 匹配一个美元符号,前提是它前面不是另一个美元符号
            )+?                            # 一次或多次非贪婪匹配
        )
        \$                                 # 以 $ 结束
    )
/ix';

其余代码及注释参考 github 仓库。

4.2 WP Editor.md 使用例

*行内公式:*
$K_k = P_{k|k-1} H_k^T (H_k P_{k|k-1} H_k^T + R_k)^{-1}$
*行外公式:*
$$K_k = P_{k|k-1} H_k^T (H_k P_{k|k-1} H_k^T + R_k)^{-1}$$

显示效果

行内公式:
K_k = P_{k|k-1} H_k^T (H_k P_{k|k-1} H_k^T + R_k)^{-1}
行外公式:
K_k = P_{k|k-1} H_k^T (H_k P_{k|k-1} H_k^T + R_k)^{-1}