本文最初是由Ashley Allen在Honeybadger 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 <script>alert("XSS Attack!");</script> 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
{
// ...
}
}
现在,我们已经为扩展名的类创建了基本轮廓,我们需要定义两个新内容:
- 解析器 - 在这里,我们将阅读降价,以找到以术语开头的任何块:
{warning}
。 - 渲染器 - 在这里,我们将定义应用于替换
{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,以及如何利用扩展以进一步丰富您的降价。