在PHP中使用Markdown
#php #markdown

本文最初是由Ashley AllenHoneybadger Developer Blog上撰写的。

Markdown是一种标记语言,对Web开发人员非常有用。它可用于编写技术文档,博客,书籍,甚至在网站上编写评论,例如GitHub。

在本文中,我们将查看什么是Markdown,使用它的好处以及如何使用PHP将Markdown转换为HTML。我们还将介绍如何创建自己的CommonMark PHP扩展名,以在Markdown文件中添加新功能和语法。

什么是降价?

在触摸任何代码之前,让我们首先看看什么是标记,其历史记录以及如何使用它的一些不同的示例。

Markdown是一种标记语言,您可以用它来创建格式的文本,例如HTML。例如,在Markdown文件中,您可以编写# Heading 1,可以将其转换为以下HTML:<h1>Heading 1</h1>。它允许您在不知道预期输出格式的情况下编写文档(在这种情况下为HTML)。它允许您创建其他元素,例如以下内容:

  • ## Heading 2将输出:<h2>Heading 2</h2>

  • **Bold text**将输出:<strong>Bold text</strong>

  • _Italic text_将输出:<em>Italic text</em>

您甚至可以像这样写表:

| Name  | Age | Favorite Color |
|-------|-----|------------------|
| Joe   | 30  | Red              |
| Alice | 41  | Green            |
| Bob   | 52  | Blue             |

该表将输出为以下HTML:

<table>
    <thead>
        <tr>
            <th>Name</th>
            <th>Age</th>
            <th>Favorite Color</th>
        </tr>
    </thead>
    <tbody>
        <tr>
            <td>Joe</td>
            <td>30</td>
            <td>Red</td>
        </tr>
        <tr>
            <td>Alice</td>
            <td>41</td>
            <td>Green</td>
        </tr>
        <tr>
            <td>Bob</td>
            <td>52</td>
            <td>Blue</td>
        </tr>
    </tbody>
</table>

Markdown最初是由John Gruber和Aaron Swartz于2004年创建的,其主要目标是可读性。他们打算将其是一种易于理解的标记语言,而无需渲染它。例如,通常,您可以清楚地看到Markdown中表的内容(如上面所示的示例),而无需首先转换和渲染。乍一看,在HTML中查看桌子并不总是那么容易理解。

首次创建了降级时,该语言的初始规范是围绕语法描述和perl脚本(称为markdown.pl)构建的。您可以通过脚本运行Markdown内容,并且它将输出HTML。但是,初始脚本在原始语法描述中具有一些错误和歧义。因此,随着脚本移植到不同的语言和软件,这导致了许多实现。这意味着通过一个转换器运行降价内容可能会导致不同的输出,而不是通过另一个转换器运行它。

因此,为了解决此问题,2014年发布了一个名为Concommark的规范。用CommonMark's自己的单词,它是“强烈定义,高度兼容的Markdown规范”。该规范旨在消除歧义性,以便无论您使用哪种共同标记兼容脚本来转换标记,输出始终相同。

CONCORMARK由github,gitlab,reddit,course,堆栈溢出和堆栈交换使用各种站点使用。因此,每当您在这些站点上编写Markdown时,它们都会使用网站规范进行转换。虽然,值得注意的是,其中一些(例如Github)使用自己的“降价”。例如,GitHub使用“ GitHub风味的Markdown”(GFM),它是一个带有额外选项(通常称为扩展名)的Commonmark的超集。因此,您可以使用现有的Commonmark功能,但还可以增加富集。为了给出一点上下文,我们将快速查看GFM中支持的内容的示例,但在常规标记规范中不支持:

GFM允许您添加罢工文本:

~~This is strikethrough~~. This is not.

使用GFM,这将导致以下输出:

<del>This is strikethrough</del>. This is not.

使用MARKDOWN的好处

作为开发人员,使用Markdown可能是非常有益的。为您的项目,软件包或库编写文档时,可以使用它。您也可以将其用于技术写作,例如博客。实际上,如果您曾经在github上阅读了您在项目中使用的软件包的“ readme”文件,则它是使用Markdown编写的。

正如我们上面已经看到的那样,Markdown可以帮助您的内容提供语义含义;在大多数情况下,您不需要渲染它才能理解它。当多人为文件做出贡献时,这很有用,因为不需要对输出的样式造型。例如,Laravel文档的内容包含在GitHub上的存储库中(laravel/docs)。任何人都可以为此做出贡献,而无需了解CSS课程或该网站在渲染过程中使用的样式。这意味着任何熟悉Markdown的人都可以直接跳入并开始以最少的阻滞剂贡献。

使用Markdown的另一个重要好处是其通常的平台不足的性质。您是否曾经在Microsoft Word中创建了一个文档,并在Google文档中打开了文档,并发现该文档看起来有所不同?也许桌子的尺寸不一样?另外,在Word中完美地进入页面末尾的文本溢出到Google文档的下一页?降级仅通过担心结构而不是样式来降低这些问题的可能性。相反,样式通常将放置在HTML输出上。

由于降价通常仅定义结构和内容而不是样式,因此降价可以转换为多种格式。因此,您可以将内容转换为HTML和其他格式,例如PDF,EPUB和MOBI。如果您使用Markdown来编写将在电子阅读器上阅读的电子书,则可能需要使用这些格式。

使用PHP中的CONCORMARK在PHP中渲染MARKDOWN

现在,我们已经简要介绍了什么是Markdown及其一些好处,让我们探索在PHP项目中使用它的方法。

为了渲染markdown文件,我们将使用league/commonmark软件包。您可以阅读完整的documentation for the package here

安装软件包

要使用Composer安装软件包,我们将使用以下命令:

composer require league/commonmark

基本用法

安装软件包后,您将能够渲染HTML:

use League\CommonMark\CommonMarkConverter;

$output = (new CommonMarkConverter())->convert('# Heading 1')->getContent();

// $output will be equal to: "<h1>Heading One</h1>"

如您所见,它很容易使用!

该包装还带有GithubFlavoredMarkdownConverter类,我们可以使用“ GitHub风味的Markdown”风味将其转换为HTML。我们可以称其与CommonMarkConvert类完全相同:

use League\CommonMark\GithubFlavoredMarkdownConverter;

$output = (new GithubFlavoredMarkdownConverter())->convert('~~This is strikethrough~~. This is not.')->getContent();

// $output will be equal to: "<del>This is strikethrough</del>. This is not."

值得注意的是,调用convert方法返回实现League\CommonMark\Output\RenderedContentInterface接口的类。除了能够调用getContent方法以获取HTML外,您还可以将对象施放到字符串中以实现相同的输出:

use League\CommonMark\GithubFlavoredMarkdownConverter;

$output = (string) (new GithubFlavoredMarkdownConverter())->convert('~~This is strikethrough~~. This is not.');

// $output will be equal to: "<del>This is strikethrough</del>. This is not."

配置和安全性

默认情况下,CommonMark PHP软件包的设计为100%符合Commonmark规范。但是,根据您的项目以及使用Markdown的方式,您可能需要更改用于html的配置。

例如,如果我们想防止<strong> html标签渲染,我们可以设置我们的配置并将其传递给我们的转换:

use League\CommonMark\CommonMarkConverter;

$config = [
    'commonmark' => [
        'enable_strong' => false,
    ]
];

$output = (new CommonMarkConverter($config))->convert('**This text is bold**')->getContent();

// $output will be equal to: "Heading One"

您可以看到,我们在$config变量中定义了配置,然后将其传递给CommonMarkConverter的构造函数。这导致未包含输出文本<strong>标签。

我们还可以使用配置来提高输出html的安全性。

例如,让我们想象我们有一个博客,我们允许读者使用Markdown评论博客文章。因此,每当读者在浏览器中加载博客文章时,也会显示评论。因为Markdown可以在其中包含HTML,因此恶意评论可能会创建跨站点脚本(XSS)攻击。

为了给这个上下文,让我们看一下Commark Markarm PHP软件包默认情况下如何转换:

use League\CommonMark\CommonMarkConverter;

$output = (new CommonMarkConverter())->convert('Before <script>alert("XSS Attack!");</script> After')->getContent();

// $output will be equal to: "Before <script>alert("XSS Attack!");</script> After"

您可以看到,<script>标签没有被删除或逃脱!因此,如果将其渲染在用户的浏览器中,则将运行<script>标签中的任何内容。

为了防止再次发生这种情况,您可以采用两种不同的方法:逃脱HTML或完全将其删除。

开始,我们可以通过将html_input配置选项设置为escape来逃脱HTML:

use League\CommonMark\CommonMarkConverter;

$output = (new CommonMarkConverter(['html_input' => 'escape']))->convert('Before <script>alert("XSS Attack!");</script>')->getContent();

// $output will be equal to: "Before &lt;script&gt;alert("XSS Attack!");&lt;/script&gt; After"

另外,如果我们想完全删除HTML,我们可以将html_input配置选项设置为strip

use League\CommonMark\CommonMarkConverter;

$output = (new CommonMarkConverter(['html_input' => 'strip']))->convert('Before <script>alert("XSS Attack!");</script>')->getContent();

// $output will be equal to: "Before  After"

有关CommonMark PHP软件包提供的配置和安全选项的完整列表,您可以check out the documentation

使用CommonMark PHP扩展

关于CommonMark软件包的很酷的事情之一是,它允许您通过添加新的语法和解析器可以使用的功能来丰富扩展。

包裹带有18个扩展名开箱即用的船,您可以在项目中立即使用。为了向您展示如何使用这些扩展名之一,我们将查看如何使用“目录”扩展程序将目录添加到我们的输出HTML中。

要开始,我们需要使用table_of_contents字段来定义我们的配置,然后将其传递到新的Markdown环境中,以便我们可以转换Markdown:

use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
use League\CommonMark\MarkdownConverter;

// Define our config...
$config = [
    'table_of_contents' => [
        'html_class' => 'table-of-contents',
        'position' => 'placeholder',
        'placeholder' => '[TOC]',
    ],
];

// Create an environment using the config...
$environment = new Environment($config);

// Register the core CommonMark parsers and renderers...
$environment->addExtension(new CommonMarkCoreExtension());

// Register the Table of Contents extension (this extension requires the HeadingPermalinkExtension!)
$environment->addExtension(new HeadingPermalinkExtension());
$environment->addExtension(new TableOfContentsExtension());

$output = (new MarkdownConverter($environment))
    ->convert(file_get_contents(__DIR__.'/markdown/article.md'))
    ->getContent();

在我们传递给环境的$config字段中,我们定义了解析器在Markdown中看到[TOC]的任何地方,它将放置一张目录并给它提供CSS级别的table-of-contents。使用CSS课程使我们能够为桌子设置样式,以适合我们预期的网站设计。附带说明,默认情况下,扩展程序将使用top的值为position,该值将直接将目录直接放置在输出的顶部,而无需包括占位符(例如[TOC])。我们还添加了HeadingPermalinkExtension扩展程序,因为TableOfContentsExtension扩展程序要求它从目录中生成链接到相关标题。

要查看此扩展程序提供的选项的完整列表,您可以查看extension's documentation

让我们想象我们传递给转换器的article.md文件包含以下内容:

[TOC]

## Programming Languages

### PHP

### Ruby

### JavaScript

这将导致以下HTML输出:

<ul class="table-of-contents">
    <li>
        <p><a href="#programming-languages">Programming Languages</a></p>
        <ul>
            <li>
                <p><a href="#php">PHP</a></p>
            </li>
        </ul>
        <ul>
            <li>
                <p><a href="#ruby">Ruby</a></p>
            </li>
        </ul>
        <ul>
            <li>
                <p><a href="#javascript">JavaScript</a></p>
            </li>
        </ul>
    </li>
</ul>

<h2 id="programming-languages">Programming Languages</h2>

<h3 id="php">PHP</h3>

<h3 id="ruby">Ruby</h3>

<h3 id="javascript">JavaScript</h3>

如您所见,从CommonMark软件包中使用扩展程序很容易开始。使用这些扩展的最大好处是,您可以在不需要过多的手动干预的情况下丰富HTML。但是,重要的是要记住,如果您要在多个位置共享此Markdown文件,则应谨慎使用(如果有)扩展名。例如,如果您在Markdown中写了一篇博客文章,然后将其交叉到许多站点,则它们可能不会支持您在自己的网站中添加的额外功能,例如添加内容表。但是,如果您将Markdown用于自己的目的,例如构建文档网站,则扩展程序可能非常强大。

创建自己的Commonmark PHP扩展

现在,我们已经研究了如何使用CommonMark软件包以及扩展名,让我们看一下如何创建自己的扩展名。出于本文的目的,我们会想象我们有一个文档网站,并且我们希望有“警告”部分,以警告开发人员常见错误或安全漏洞。因此,我们会说,我们在代码中看到{warning}的任何地方都想在html中输出警告。

首先,要创建扩展名,我们需要创建一个实现Commark Package的League\CommonMark\Extension\ExtensionInterface接口的类。此类仅包含一个接受League\CommonMark\Environment\ConfigurableEnvironmentInterface实例的单个register方法。因此,班级的样板看起来像这样:

namespace App\Markdown\Extensions;

use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Extension\ExtensionInterface;

class WarningExtension implements ExtensionInterface
{
    public function register(EnvironmentBuilderInterface $environment): void
    {
        // ...
    }
}

现在,我们已经为扩展名的类创建了基本轮廓,我们需要定义两个新内容:

  1. 解析器 - 在这里,我们将阅读降价,以找到以术语开头的任何块:{warning}
  2. 渲染器 - 在这里,我们将定义应用于替换{warning}的HTML。

我们将从定义解析器类开始:

namespace App\Markdown\Extensions;

use League\CommonMark\Parser\Block\BlockStart;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Cursor;
use League\CommonMark\Parser\MarkdownParserStateInterface;

class WarningParser implements BlockStartParserInterface
{
    public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
    {
        // Does the block start with {warning}?
        if (!str_starts_with($cursor->getRemainder(), '{warning}')) {
            return BlockStart::none();
        }

        // The block starts with {warning}, so remove it from the string.
        $warningMessage = str_replace('{warning} ', '', $cursor->getRemainder());

        return BlockStart::of(new WarningBlockParser($warningMessage));
    }
}

我们的WarningParser类将在滚动中的每个块中循环时使用。它将检查块是否以{warning}开头。如果不是这样,我们将返回null(通过BlockStart::none()方法)。如果块确实以{warning}开头,我们将从字符串中删除它以查找警告消息。例如,如果我们的宣传是{warning} My warning here,则警告消息将为My warning here

然后,我们将警告消息传递给WarningBlockParser类,然后将其传递给BlockStart::of()方法。我们的WarningBlockParser类实现了BlockContinueParserInterface,因此我们必须实施几种方法。我们的WarningBlockParser看起来像这样:

namespace App\Markdown\Extensions;

use League\CommonMark\Node\Block\AbstractBlock;
use League\CommonMark\Parser\Block\BlockContinue;
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
use League\CommonMark\Parser\Cursor;

class WarningBlockParser implements BlockContinueParserInterface
{
    private Warning $warning;

    public function __construct(string $warningMessage)
    {
        $this->warning = new Warning($warningMessage);
    }

    public function getBlock(): AbstractBlock
    {
        return $this->warning;
    }

    public function isContainer(): bool
    {
        return false;
    }

    public function canHaveLazyContinuationLines(): bool
    {
        return false;
    }

    public function canContain(AbstractBlock $childBlock): bool
    {
        return false;
    }

    public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
    {
        return BlockContinue::none();
    }

    public function addLine(string $line): void
    {
        //
    }

    public function closeBlock(): void
    {
        //
    }
}

该方法的重要部分是我们正在返回一个Warning类,该类别从getBlock方法中实现AbstractBlock接口。我们的Warning课程看起来像这样:

namespace App\Markdown\Extensions;

use League\CommonMark\Node\Block\AbstractBlock;

class Warning extends AbstractBlock
{
    public function __construct(private string $warningMessage)
    {
        parent::__construct();
    }

    public function getHtml(): string
    {
        return '<div class="warning">'.$this->warningMessage.'</div>';
    }
}

您可以看到,我们正在返回getHtml方法中的HTML。出于此示例的目的,HTML仅包含一个带有warning类的<div>,但是您可以将其更改为说您喜欢的任何内容。

现在我们已经创建了解析器并定义了应该返回的HTML,我们需要创建渲染器类:

namespace App\Markdown\Extensions;

use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;

class WarningRenderer implements NodeRendererInterface
{
    /**
     * @param Warning $node
     *
     * {@inheritDoc}
     */
    public function render(Node $node, ChildNodeRendererInterface $childRenderer)
    {
        return $node->getHtml();
    }
}

WarningRenderer类中的render方法只需调用并返回我们的Warning类中的getHtml方法即可。因此,此渲染器类只会返回HTML作为字符串。

现在我们已经创建了解析器和渲染器类,我们可以将它们添加到我们的WarningExtension扩展类中:

namespace App\Markdown\Extensions;

use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Environment\ConfigurableEnvironmentInterface;

class WarningExtension implements ExtensionInterface
{
    public function register(ConfigurableEnvironmentInterface $environment): void
    {
        $environment->addInlineParser(new WarningParser())
            ->addInlineRenderer(new WarningRenderer());
    }
}

现在我们已经完成了扩展名,我们可以在环境中进行注册:

use App\Markdown\Extensions\WarningExtension;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\TableOfContents\TableOfContentsExtension;
use League\CommonMark\MarkdownConverter;

$environment = new Environment();

// Register the core CommonMark parsers and renderers...
$environment->addExtension(new CommonMarkCoreExtension());

// Register our new WarningExtension
$environment->addExtension(new WarningExtension());

$output = (new MarkdownConverter($environment))
    ->convert(file_get_contents(__DIR__.'/markdown/article.md'))
    ->getContent();

让我们想象我们传递给转换器的article.md文件包含以下内容:

This is some text about a security-related issue.

{warning} This is the warning text

This is after the warning text.

这将导致以下HTML输出:

This is some text about a security-related issue.

<div class="warning">This is the warning text</div>

This is after the warning text.

结论

希望,本文帮助您了解了什么是Markdown及其好处。它还应该让您了解如何在PHP项目中安全使用Markdown来使用CommonMark PHP渲染HTML,以及如何利用扩展以进一步丰富您的降价。