问题场景和效果对比

测试代码

- [ ] 未完成的任务
- [x] 已完成的任务

1. [ ] 有序列表中的未完成任务
2. [x] 有序列表中的已完成任务

原效果:

修复效果:

  • 未完成的任务
  • 已完成的任务
  1. 有序列表中的未完成任务
  2. 有序列表中的已完成任务

原始代码概览

<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;

/**
 * Markdown解析
 *
 * @package Markdown
 * @copyright Copyright (c) 2014 Typecho team (http://www.typecho.org)
 * @license GNU General Public License 2.0
 */
class Markdown
{
    /**
     * convert 
     * 
     * @param string $text 
     * @return string
     */
    public static function convert($text)
    {
        static $parser;

        if (empty($parser)) {
            $parser = new HyperDown();

            $parser->hook('afterParseCode', function ($html) {
                return preg_replace("/<code class=\"([_a-z0-9-]+)\">/i", "<code class=\"lang-\\1\">", $html);
            });

            $parser->hook('beforeParseInline', function ($html) use ($parser) {
                return preg_replace_callback("/^\s*<!\-\-\s*more\s*\-\->\s*$/s", function ($matches) use ($parser) {
                    return $parser->makeHolder('<!--more-->');
                }, $html);
            });

            $parser->enableHtml(true);
            $parser->_commonWhiteList .= '|img|cite|embed|iframe';
            $parser->_specialWhiteList = array_merge($parser->_specialWhiteList, array(
                'ol'            =>  'ol|li',
                'ul'            =>  'ul|li',
                'blockquote'    =>  'blockquote',
                'pre'           =>  'pre|code'
            ));
        }

        return str_replace('<p><!--more--></p>', '<!--more-->', $parser->makeHtml($text));
    }

    /**
     * transerCodeClass
     * 
     * @param string $html
     * @return string
     */
    public static function transerCodeClass($html)
    {
        return preg_replace("/<code class=\"([_a-z0-9-]+)\">/i", "<code class=\"lang-\\1\">", $html);
    }

    /**
     * @param $html
     * @return mixed
     */
    public static function transerComment($html)
    {
        return preg_replace_callback("/<!\-\-(.+?)\-\->/s", array('Markdown', 'transerCommentCallback'), $html);
    }

    /**
     * @param $matches
     * @return string
     */
    public static function transerCommentCallback($matches)
    {
        return self::$parser->makeHolder($matches[0]);
    }
}
?>

二、问题发现与详细分析

1. 任务列表(Task Lists)支持不足

问题描述

在使用现有的Markdown解析器时,发现无法正确渲染任务列表(如- [ ]- [x])。

分析疑问

  • 疑问1:当前的解析器是否支持任务列表的语法?
  • 疑问2:如果不支持,是什么原因导致任务列表无法正确渲染?
  • 疑问3:是否有现有的代码部分可以复用或修改以支持任务列表?

发现原因

在原始代码中,解析器主要关注代码块(<code>标签)和特定注释的处理,没有针对任务列表的专门处理。具体表现为:

  • 缺乏任务列表的正则匹配:现有的convert方法中,使用了preg_replace来替换<!--more-->标签,但没有对任务列表的- [ ]- [x]进行任何处理。
  • 代码重复:即使在后续的transerCodeClasstranserComment方法中进行了一些处理,但依然没有覆盖到任务列表的转换。

2. 代码结构冗余与可维护性差

问题描述

在原始代码中,处理任务列表的逻辑(尽管未实现)如果需要添加,可能会涉及到重复的正则表达式处理。例如,无序列表(ul)和有序列表(ol)的处理可能会重复,增加代码的复杂性和维护难度。

分析疑问

  • 疑问1:现有代码中是否存在重复的逻辑?
  • 疑问2:是否可以将重复的部分抽象为独立的函数,以提高代码的可读性和维护性?
  • 疑问3:代码结构是否符合单一职责原则,是否有优化空间?

发现原因

  • 重复正则表达式:原始代码中的任务列表处理逻辑可能需要在无序列表和有序列表中分别进行匹配和替换,导致相同或相似的正则表达式在多个地方重复出现。
  • 函数职责不明确convert方法负责多个任务,包括初始化解析器、处理代码块、注释以及其他HTML标签的转换,职责过于集中,难以维护和扩展。

3. 解析器初始化效率低下

问题描述

在原始代码中,解析器实例使用了static变量$parser,并在每次调用convert方法时检查是否已初始化。这种方式在高频调用的情况下,可能会带来性能上的浪费,尤其是在多线程或高并发环境下。

分析疑问

  • 疑问1:每次调用convert方法时,解析器的初始化是否会带来性能开销?
  • 疑问2:是否有更高效的方式来管理解析器实例,避免重复初始化?
  • 疑问3:当前的单例模式是否符合最佳实践,是否存在改进空间?

发现原因

  • 性能开销:虽然使用static变量可以避免每次调用都初始化解析器,但在代码层面仍然存在潜在的性能瓶颈,尤其是在并发环境下。
  • 单例模式局限:当前的实现方式简单,但缺乏灵活性,如无法在不同的上下文中使用不同的解析器配置。

三、详细的解决方案

针对上述发现的问题,我们制定了以下详细的解决方案,具体到每个疑问和对应的改进措施。

1. 增加任务列表的支持

改进措施

  • 新增任务列表处理函数:编写一个专门处理任务列表的函数,通过正则表达式匹配- [ ]- [x],并将其转换为HTML的复选框。
  • 集成到转换流程中:在convert方法中,解析完成后调用新增的任务列表处理函数,确保任务列表在最终HTML中正确渲染。

具体实现

在改进后的代码中,新增了convertTaskLists方法,专门负责将Markdown中的任务列表语法转换为HTML复选框:

/**
 * 处理任务列表,将 - [ ] 和 - [x] 转换为HTML复选框
 * 
 * @param string $html 解析后的HTML
 * @return string 处理后的HTML
 */
private static function convertTaskLists($html)
{
    // 定义正则表达式模式,匹配任务列表项
    $pattern = '/<li>\s*\[(x| )\]\s+(.*?)<\/li>/i';

    // 回调函数,用于替换匹配的任务列表项
    $callback = function ($matches) {
        // 判断是否为选中的复选框
        $checked = strtolower($matches[1]) === 'x' ? 'checked' : '';
        // 对任务文本进行HTML实体编码,防止XSS攻击
        $taskText = htmlspecialchars($matches[2], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
        // 返回带有复选框的列表项HTML
        return '<li><input type="checkbox" disabled ' . $checked . ' /> ' . $taskText . '</li>';
    };

    // 使用正则表达式替换所有匹配的任务列表项
    $html = preg_replace_callback($pattern, $callback, $html);

    return $html;
}

convert方法中调用该函数:

// 处理任务列表,将 - [ ] 和 - [x] 转换为复选框
$html = self::convertTaskLists($html);

2. 优化代码结构,提高可维护性

改进措施

  • 抽象重复逻辑:将任务列表的处理逻辑抽象为独立的函数,避免在不同类型的列表中重复编写相似的正则表达式。
  • 单一职责原则:将convert方法的职责划分明确,解析器初始化、任务列表处理、特定标签替换等功能分别由独立的方法负责。
  • 提高代码可读性:通过清晰的函数命名和注释,使代码逻辑更加直观,便于后续维护和扩展。

具体实现

在改进后的代码中,convertTaskLists方法独立处理任务列表,convert方法则专注于整体的转换流程:

/**
 * 将Markdown文本转换为HTML
 * 
 * @param string $text Markdown文本
 * @return string 转换后的HTML
 */
public static function convert($text)
{
    // 检查解析器是否已初始化
    if (empty(self::$parser)) {
        self::$parser = new HyperDown();

        // 钩子:在解析代码块后处理
        self::$parser->hook('afterParseCode', function ($html) {
            // 将代码块的class属性从"language-xxx"改为"lang-xxx"
            return self::transerCodeClass($html);
        });

        // 钩子:在解析内联元素前处理
        self::$parser->hook('beforeParseInline', function ($html) {
            // 将特定的注释标记转换为占位符
            return self::transerComment($html);
        });

        // 启用HTML标签解析
        self::$parser->enableHtml(true);

        // 扩展允许的通用白名单标签
        self::$parser->_commonWhiteList .= '|img|cite|embed|iframe';

        // 扩展特殊白名单标签及其子标签
        self::$parser->_specialWhiteList = array_merge(self::$parser->_specialWhiteList, array(
            'ol'            =>  'ol|li',
            'ul'            =>  'ul|li',
            'blockquote'    =>  'blockquote',
            'pre'           =>  'pre|code'
        ));
    }

    // 使用 HyperDown 解析 Markdown 文本
    $html = self::$parser->makeHtml($text);

    // 替换特定的占位符标签
    $html = str_replace('<p><!--more--></p>', '<!--more-->', $html);

    // 处理任务列表,将 - [ ] 和 - [x] 转换为复选框
    $html = self::convertTaskLists($html);

    return $html;
}

3. 提升解析器初始化的效率

改进措施

  • 使用私有静态变量:将解析器实例$parser设为私有静态变量,确保在整个应用生命周期中只初始化一次,避免多次初始化带来的性能开销。
  • 单例模式优化:采用更严格的单例模式,防止在其他地方意外创建多个解析器实例,确保资源的高效利用。
  • 懒加载:仅在需要时初始化解析器,避免不必要的资源占用。

具体实现

在改进后的代码中,解析器实例被设为私有静态变量$parser,并通过检查是否为空来决定是否初始化:

/**
 * Markdown解析器实例
 *
 * @var HyperDown
 */
private static $parser;

convert方法中进行初始化:

if (empty(self::$parser)) {
    self::$parser = new HyperDown();
    // 初始化钩子和白名单
    // ...
}

四、改进后的代码

<?php
if (!defined('__TYPECHO_ROOT_DIR__')) exit;

/**
 * Markdown解析
 *
 * @package Markdown
 * @copyright Copyright (c) 2014 Typecho team (http://www.typecho.org)
 * @license GNU General Public License 2.0
 */
class Markdown
{
    /**
     * Markdown解析器实例
     *
     * @var HyperDown
     */
    private static $parser;

    /**
     * 将Markdown文本转换为HTML
     * 
     * @param string $text Markdown文本
     * @return string 转换后的HTML
     */
    public static function convert($text)
    {
        // 检查解析器是否已初始化
        if (empty(self::$parser)) {
            self::$parser = new HyperDown();

            // 钩子:在解析代码块后处理
            self::$parser->hook('afterParseCode', function ($html) {
                // 将代码块的class属性从"language-xxx"改为"lang-xxx"
                return self::transerCodeClass($html);
            });

            // 钩子:在解析内联元素前处理
            self::$parser->hook('beforeParseInline', function ($html) {
                // 将特定的注释标记转换为占位符
                return self::transerComment($html);
            });

            // 启用HTML标签解析
            self::$parser->enableHtml(true);

            // 扩展允许的通用白名单标签
            self::$parser->_commonWhiteList .= '|img|cite|embed|iframe';

            // 扩展特殊白名单标签及其子标签
            self::$parser->_specialWhiteList = array_merge(self::$parser->_specialWhiteList, array(
                'ol'            =>  'ol|li',
                'ul'            =>  'ul|li',
                'blockquote'    =>  'blockquote',
                'pre'           =>  'pre|code'
            ));
        }

        // 使用 HyperDown 解析 Markdown 文本
        $html = self::$parser->makeHtml($text);

        // 替换特定的占位符标签
        $html = str_replace('<p><!--more--></p>', '<!--more-->', $html);

        // 处理任务列表,将 - [ ] 和 - [x] 转换为复选框
        $html = self::convertTaskLists($html);

        return $html;
    }

    /**
     * 处理任务列表,将 - [ ] 和 - [x] 转换为HTML复选框
     * 
     * @param string $html 解析后的HTML
     * @return string 处理后的HTML
     */
    private static function convertTaskLists($html)
    {
        // 定义正则表达式模式,匹配任务列表项
        $pattern = '/<li>\s*\[(x| )\]\s+(.*?)<\/li>/i';

        // 回调函数,用于替换匹配的任务列表项
        $callback = function ($matches) {
            // 判断是否为选中的复选框
            $checked = strtolower($matches[1]) === 'x' ? 'checked' : '';
            // 对任务文本进行HTML实体编码,防止XSS攻击
            $taskText = htmlspecialchars($matches[2], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
            // 返回带有复选框的列表项HTML
            return '<li><input type="checkbox" disabled ' . $checked . ' /> ' . $taskText . '</li>';
        };

        // 使用正则表达式替换所有匹配的任务列表项
        $html = preg_replace_callback($pattern, $callback, $html);

        return $html;
    }

    /**
     * 转换代码块的class属性
     * 
     * @param string $html 解析后的HTML
     * @return string 转换后的HTML
     */
    public static function transerCodeClass($html)
    {
        return preg_replace("/<code class=\"([_a-z0-9-]+)\">/i", "<code class=\"lang-\\1\">", $html);
    }

    /**
     * 转换HTML注释为占位符
     * 
     * @param string $html 解析后的HTML
     * @return mixed 转换后的HTML
     */
    public static function transerComment($html)
    {
        return preg_replace_callback("/<!\-\-(.+?)\-\->/s", array('Markdown', 'transerCommentCallback'), $html);
    }

    /**
     * 注释转换的回调函数
     * 
     * @param array $matches 正则表达式匹配结果
     * @return string 占位符字符串
     */
    public static function transerCommentCallback($matches)
    {
        return self::$parser->makeHolder($matches[0]);
    }
}
?>

五、改进后的代码运行流程

  1. 初始化解析器

    • 在第一次调用convert方法时,检查self::$parser是否为空。
    • 若为空,实例化HyperDown解析器,并设置相关钩子和白名单。
  2. Markdown转换

    • 使用self::$parser->makeHtml($text)将Markdown文本转换为HTML。
  3. 特定标签替换

    • 替换<p><!--more--></p><!--more-->,确保<!--more-->标签的正确位置。
  4. 任务列表处理

    • 调用convertTaskLists函数,将HTML中的任务列表语法转换为复选框。
  5. 返回最终HTML

    • 返回处理后的HTML,供前端展示或其他用途。