我一直在寻找一个很棒的代码高亮器,我一直在使用很多我什至不记得的东西。这些是我现在能记住的:
- SyntaxHighlighter
- Google Prettifier
- highlighter.js
- Geshi
现在我正在使用 highlighter.js
但这并不是我想要的,我想要的是能够突出显示大多数“单词”或保留字,例如 built in功能,对象等,这个荧光笔和大部分都丢失了。我知道这不是一件重要的事情,不幸的是,这一直困扰着我,直到现在。
最后,我发现 Pygments 是与我一直在寻找的东西相匹配的完美选择,它与 GitHub 使用的相同。现在唯一的障碍是它是一个基于 python 的语法荧光笔,而我使用的是 WordPress,而 WordPress 是基于 PHP 构建的。
安装
但是,嘿,我们可以克服它,有一个解决方案,首先,我们需要在我们的服务器上安装 python 以便我们可以使用 Pygments。
我们不会深入介绍安装,因为存在如此多的操作系统风格,并且每个操作系统风格可能略有不同。
蟒蛇
首先,您必须通过在命令行中输入 python
来检查您是否已经安装了 python。
如果未安装,您应该查看 Python 下载页面并下载您的操作系统安装程序。
画中画安装程序
根据其站点安装pip installer,有两种安装方式:
第一个也是推荐的方法是下载 get-pip.py 并在命令行上运行它:
python get-pip.py
第二种方法是使用包管理器,通过运行这两个可能的命令之一,就像之前提到的那样,这取决于您的服务器操作系统。
sudo apt-get install python-pip
或者:
sudo yum install python-pip
注意:您可以使用您喜欢的任何包管理器,例如 easy_install,为了举例,因为是 Pygments 站点上使用的那个,我使用了 pip。
颜料
要安装 pygments,你需要运行这个命令:
pip install Pygments
如果您所在的服务器上的用户没有 root 访问权限,您将无法使用前面的命令安装它,如果是这种情况,您必须使用 --user
在用户目录上安装模块的标志。
pip install --user Pygments
现在一切都已安装,所以我们要做的是使用 PHP 和一些 Python 代码
PHP + Python
它的工作方式是通过 php 使用 exec()
发送语言名称和包含要突出显示的代码的文件的文件名来执行 python 脚本。
蟒蛇
我们要做的第一件事是创建 python 脚本,该脚本将使用 Pygments 将纯代码转换为突出显示的代码。
那么让我们逐步了解如何创建 python 脚本。
首先我们导入所有需要的模块:
import sys from pygments import highlight from pygments.formatters import HtmlFormatter
sys
模块提供 argv
列表,其中包含传递给 python 脚本的所有参数。
pygments 中的
highlight
实际上是 main 函数以及 lexer 将生成突出显示的代码。您会在下面阅读更多关于词法分析器的内容。
HtmlFormatter
是我们希望生成的代码格式化的方式,我们将使用 HTML 格式。以下是可用格式化程序的列表,以备不时之需。
# Get the code language = (sys.argv[1]).lower() filename = sys.argv[2] f = open(filename, 'rb') code = f.read() f.close()
此代码块的作用是获取第二个参数 (sys.argv[1]
) 并将其转换为小写文本,以确保它始终为小写。因为 "php" !== "PHP"
。第三个参数 sys.argv[2]
是代码的文件名路径,所以我们打开,读取它的内容并关闭它。第一个参数是 python 的脚本名称。
# Importing Lexers # PHP if language == 'php': from pygments.lexers import PhpLexer lexer = PhpLexer(startinline=True) # GUESS elif language == 'guess': from pygments.lexers import guess_lexer lexer = guess_lexer( code ) # GET BY NAME else: from pygments.lexers import get_lexer_by_name lexer = get_lexer_by_name( language )
所以是时候导入词法分析器了,这段代码的作用是根据我们需要分析的语言创建词法分析器。词法分析器它的作用是分析我们的代码并获取每个保留字、符号、内置函数等。
在这种情况下,在词法分析器分析之后,所有代码都将格式化为 HTML,将所有“单词”包装到带有类的 HTML 元素中。顺便说一句,类名根本没有描述性,所以函数不是类“函数”,但无论如何,现在这不是什么值得担心的事情。
变量 language
包含我们要转换代码的语言名称的字符串,我们使用 lexer = get_lexer_by_name( language )
通过名称获取任何词法分析器,好吧,它的功能不言自明。但是为什么我们首先检查 php 和 guess 你可能会问,好吧,我们检查 php 因为如果我们使用 get_lexer_by_name('php')
并且 php 代码没有所需的开始 php 标记 <?php 不会很好地突出显示代码或如我们预期的那样,我们需要创建一个特定的 php像这样的词法分析器 lexer = PhpLexer(startinline=True)
传递 startinline=True
作为参数,所以不再需要这个开始的 php 标签。 guess
是我们从 php 传递给 pygments 的字符串,我们不知道它是哪种语言,或者没有提供语言,我们需要猜测它。
在他们的网站上有一个可用的词法分析器列表。
python 的最后一步是创建 HTML 格式化程序,执行突出显示并输出包含突出显示代码的 HTML 代码。
formatter = HtmlFormatter(linenos=False, encoding='utf-8', nowrap=True) highlighted = highlight(code, lexer, formatter) print highlighted
对于格式化程序,传递 linenos=False
以不生成行号,传递 nowrap=True
以不允许 div 包装生成代码。这是个人决定,代码将使用 PHP 进行包装。
接下来它传递包含实际代码的 code
,包含语言词法分析器的 lexer
和我们刚刚在上面的行中创建的 formatter
突出显示我们希望如何格式化代码。
最后输出代码。
这就是关于 python 的,即要构建亮点的脚本。
这是完整的文件:build.py
import sys from pygments import highlight from pygments.formatters import HtmlFormatter # If there isn't only 2 args something weird is going on expecting = 2; if ( len(sys.argv) != expecting + 1 ): exit(128) # Get the code language = (sys.argv[1]).lower() filename = sys.argv[2] f = open(filename, 'rb') code = f.read() f.close() # PHP if language == 'php': from pygments.lexers import PhpLexer lexer = PhpLexer(startinline=True) # GUESS elif language == 'guess': from pygments.lexers import guess_lexer lexer = guess_lexer( code ) # GET BY NAME else: from pygments.lexers import get_lexer_by_name lexer = get_lexer_by_name( language ) # OUTPUT formatter = HtmlFormatter(linenos=False, encoding='utf-8', nowrap=True) highlighted = highlight(code, lexer, formatter) print highlighted
PHP-WordPress
让我们跳转到 WordPress 并创建一个基本插件来处理需要突出显示的代码。
如果你一辈子都没有为WordPress创建过插件也没关系,这个插件只是一个文件,里面有php函数,所以你没有WordPress插件开发知识也没关系,但你需要知识尽管在 WordPress 开发上。
在 wp-content/plugins
中创建一个名为 wp-pygments 的文件夹(可以是任何你想要的)并在其中复制 build.py 我们刚刚创建的 python 脚本并创建一个名为 wp-pygments.php 的新 php 文件(可能与目录同名)。
下面的代码只是让 WordPress 知道插件的名称和其他信息,这段代码将位于 wp-pygments.php 的顶部。
<?php /* * Plugin Name: WP Pygments * Plugin URI: http://wellingguzman.com/wp-pygments * Description: A brief description of the Plugin. * Version: 0.1 * Author: Welling Guzman * Author URI: http://wellingguzman.com * License: MEH */ ?>
在 the_content
上添加过滤器以查找
标签。预期的代码是:<pre > <code> $name = "World"; echo "Hello, " . $name; </code> </pre>注意:html标签需要编码;例如
<
需要是<
这样解析就不会混淆,也不会做错了。其中
class
是pre标签中代码的语言,如果没有class或者是空的会把guess
传递给build.py
.add_filter( 'the_content', 'mb_pygments_content_filter' ); function mb_pygments_content_filter( $content ) { $content = preg_replace_callback('/]?.*?>.*?<code>(.*?)<\/code>.*?<\/pre>/sim', 'mb_pygments_convert_code', $content); return $content; }
preg_replace_callback
函数将在每次使用提供的正则表达式模式匹配内容时执行mb_pygments_convert_code
回调函数:/
]?.*?>.*?(.*?).*?/sim
,它应该匹配帖子/页面内容上的任何。sim 呢?这是三个模式修饰符标志。来自 php.net:
- s:如果设置了这个修饰符,则模式中的点元字符匹配所有字符,包括换行符。
- i:如果设置了此修饰符,则模式中的字母同时匹配大写和小写字母。
- m:默认情况下,PCRE 将主题字符串视为由单个“行”组成字符(即使它实际上包含几个换行符)。
这也可以用 DOMDocument();
来完成。替换 /
]?.*?>.*?
(.*?).*?/sim与此:
// This prevent throwing error libxml_use_internal_errors(true); // Get all pre from post content $dom = new DOMDocument(); $dom->loadHTML($content); $pres = $dom->getElementsByTagName('pre'); foreach ($pres as $pre) { $class = $pre->attributes->getNamedItem('class')->nodeValue; $code = $pre->nodeValue; $args = array( 2 => $class, // Element at position [2] is the class 3 => $code // And element at position [2] is the code ); // convert the code $new_code = mb_pygments_convert_code($args); // Replace the actual pre with the new one. $new_pre = $dom->createDocumentFragment(); $new_pre->appendXML($new_code); $pre->parentNode->replaceChild($new_pre, $pre); } // Save the HTML of the new code. $content = $dom->saveHTML();
下面的代码来自 mb_pygments_convert_code
函数。
define( 'MB_WPP_BASE', dirname(__FILE__) ); function mb_pygments_convert_code( $matches ) { $pygments_build = MB_WPP_BASE . '/build.py'; $source_code = isset($matches[3])?$matches[3]:''; $class_name = isset($matches[2])?$matches[2]:''; // Creates a temporary filename $temp_file = tempnam(sys_get_temp_dir(), 'MB_Pygments_'); // Populate temporary file $filehandle = fopen($temp_file, "w"); fwrite($filehandle, html_entity_decode($source_code, ENT_COMPAT, 'UTF-8') ); fclose($filehandle); // Creates pygments command $language = $class_name?$class_name:'guess'; $command = sprintf('python %s %s %s', $pygments_build, $language, $temp_file); // Executes the command $retVal = -1; exec( $command, $output, $retVal ); unlink($temp_file); // Returns Source Code $format = '<div ><pre><code>%s</code></pre></div>'; if ( $retVal == 0 ) $source_code = implode("\n", $output); $highlighted_code = sprintf($format, $language, $source_code); return $highlighted_code; }
查看上面的代码:
define( 'MB_WPP_BASE', dirname(__FILE__) );
定义一个绝对插件的目录路径常量。
$pygments_build = MB_WPP_BASE . '/build.py'; $source_code = isset($matches[3])?$matches[3]:''; $class_name = isset($matches[2])?$matches[2]:'';
$pygments_build
是python脚本所在的完整路径。每次匹配时,都会传递一个名为 $matches
的数组,其中包含 4 个元素。以此作为来自帖子/页面内容的匹配代码的示例:
<pre > <code> $name = "World"; echo "Hello, " . $name; </code> </pre>
-
位置[0]的元素为整个
<pre > <code> $name = "World"; echo "Hello, " . $name; </code> </pre>
-
位置[1]的元素是类属性名及其值,其值为:
-
位置[2]的元素是没有名字的类属性值,它的值为:
php
- p>位置[3]的元素是去掉
pre
标签的代码本身,其值为:$name = "World"; echo "Hello, " . $name;
// Creates a temporary filename $temp_file = tempnam(sys_get_temp_dir(), 'MB_Pygments_');
它创建一个临时文件,其中包含将传递给 python 脚本的代码。这是处理代码的更好方法。如果不将整个事情作为参数传递,那将是一团糟。
// Populate temporary file $filehandle = fopen($temp_file, "wb"); fwrite($filehandle, html_entity_decode($source_code, ENT_COMPAT, 'UTF-8') ); fclose($filehandle);
它创建代码文件,但我们解码所有 HTML 实体,因此 pygments 可以正确转换它们。
// Creates pygments command $language = $class_name?$class_name:'guess'; $command = sprintf('python %s %s %s', $pygments_build, $language, $temp_file);
它创建要使用的 python 命令,它输出:
python /path/to/build.py php /path/to/temp.file
// Executes the command $retVal = -1; exec( $command, $output, $retVal ); unlink($temp_file); // Returns Source Code $format = '<div ><pre><code>%s</code></pre></div>'; if ( $retVal == 0 ) $source_code = implode("\n", $output); $highlighted_code = sprintf($format, $language, $source_code);
执行刚刚创建的命令,如果返回 0,则在 python 脚本上一切正常。 exec();
返回 python 脚本输出的行数组。所以我们将数组输出连接成一个字符串作为源代码。如果没有,我们将坚持使用没有突出显示的代码。
通过缓存改进
现在工作正常,但我们必须节省时间和处理,想象一下内容上的 100 个
标签会创建 100 个文件并调用 100 次 python 脚本,所以让我们缓存这个宝贝。瞬态 API
WordPress 使用 Transient API 提供在数据库中临时存储数据的能力。
首先,让我们向
save_post
挂钩添加一个操作,这样每次保存帖子时我们都会转换代码并缓存它。add_action( 'save_post', 'mb_pygments_save_post' ); function mb_pygments_save_post( $post_id ) { if ( wp_is_post_revision( $post_id ) ) return; $content = get_post_field( 'post_content', $post_id ); mb_pygments_content_filter( $content ); }如果是修订我们什么都不做,否则我们获取帖子内容并调用 pygments 内容过滤器函数。
让我们创建一些函数来处理缓存。
// Cache Functions // Expiration time (1 month), let's clear cache every month. define('MB_WPP_EXPIRATION', 60 * 60 * 24 * 30); // This function it returns the name of a post cache. function get_post_cache_transient() { global $post; $post_id = $post->ID; $transient = 'post_' . $post_id . '_content'; return $transient; } // This creates a post cache for a month, // containing the new content with pygments // and last time the post was updated. function save_post_cache($content) { global $post; $expiration = MB_WPP_EXPIRATION; $value = array( 'content'=>$content, 'updated'=>$post->post_modified ); set_transient( get_post_cache_transient(), $value, $expiration ); } // This returns a post cache function get_post_cache() { $cached_post = get_transient( get_post_cache_transient() ); return $cached_post; } // Check if a post needs to be updated. function post_cache_needs_update() { global $post; $cached_post = get_post_cache(); if ( strtotime($post->post_modified) > strtotime($cached_post['updated']) ) return TRUE; return FALSE; } // Delete a post cache. function clear_post_cache() { delete_transient( get_post_cache_transient() ); }在
mb_pygments_content_filter()
的开头添加一些行来检查是否有缓存的帖子。function mb_pygments_content_filter( $content ) { if ( FALSE !== ( $cached_post = get_post_cache() ) && !post_cache_needs_update() ) return $cached_post['content']; clear_post_cache();并在
mb_pygments_content_filter()
的末尾添加一行以保存帖子缓存。save_post_cache( $content );最后,当插件被卸载时,我们需要删除所有我们创建的缓存,这有点棘手,所以我们使用
$wpdb
对象来删除所有使用这个查询。register_uninstall_hook(__FILE__, 'mb_wp_pygments_uninstall'); function mb_wp_pygments_uninstall() { global $wpdb; $wpdb->query( "DELETE FROM `wp_options` WHERE option_name LIKE '_transient_post_%_content' " ); }