Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi

## [Unreleased][unreleased]

### Added
- Added a new `HighlightExtension` for marking important text using `==` syntax (#1100)

## [2.7.0]

This is a **security release** to address a potential cross-site scripting (XSS) vulnerability when using the `AttributesExtension` with untrusted user input.
Expand Down
58 changes: 58 additions & 0 deletions docs/2.x/extensions/highlight.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
layout: default
title: Highlight Extension
description: The HighlightExtension allows marking important text.
redirect_from:
- /extensions/highlight/
---

# Highlight Extension

This extension adds support for highlighting important text using the `==` syntax. For example, the Markdown:

```markdown
I need to highlight these ==very important words==.
```

Would be rendered to HTML as:

```html
<p>I need to highlight these <mark>very important words</mark>.</p>
```

Which could then be styled using CSS to produce a highlighter effect.

## Installation

This extension is bundled with `league/commonmark`. This library can be installed via Composer:

```bash
composer require league/commonmark
```

See the [installation](/2.x/installation/) section for more details.

## Usage

This extension can be added to any new `Environment`:

```php
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\Highlight\HighlightExtension;
use League\CommonMark\MarkdownConverter;

// Define your configuration, if needed
$config = [];

// Configure the Environment with all the CommonMark parsers/renderers
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());

// Add this extension
$environment->addExtension(new HighlightExtension());

// Instantiate the converter engine and start converting some Markdown!
$converter = new MarkdownConverter($environment);
echo $converter->convert('I need to highlight these ==very important words==.');
```
2 changes: 2 additions & 0 deletions docs/2.x/extensions/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ to enhance your experience out-of-the-box depending on your specific use-cases.
| [Front Matter] | Parses YAML front matter from your Markdown input | `2.0.0` | |
| **[GitHub Flavored Markdown]** | Enables full support for GFM. Automatically includes the extensions noted in the `GFM` column (though you can certainly add them individually if you wish): | `1.3.0` | |
| [Heading Permalinks] | Makes heading elements linkable | `1.4.0` | |
| [Highlight] | Mark text as being highlighted for reference or notation purposes | `2.8.0` | |
| [Inlines Only] | Only includes standard CommonMark inline elements - perfect for handling comments and other short bits of text where you only want bold, italic, links, etc. | `1.3.0` | |
| [Mentions] | Easy parsing of `@mention` and `#123`-style references | `1.5.0` | |
| [Strikethrough] | Allows using tilde characters (`~~`) for ~strikethrough~ formatting | `1.3.0` | <i class="fab fa-github"></i> |
Expand Down Expand Up @@ -121,6 +122,7 @@ See the [Custom Extensions](/2.x/customization/extensions/) page for details on
[Front Matter]: /2.x/extensions/front-matter/
[GitHub Flavored Markdown]: /2.x/extensions/github-flavored-markdown/
[Heading Permalinks]: /2.x/extensions/heading-permalinks/
[Highlight]: /2.x/extensions/highlight/
[Inlines Only]: /2.x/extensions/inlines-only/
[Mentions]: /2.x/extensions/mentions/
[Strikethrough]: /2.x/extensions/strikethrough/
Expand Down
1 change: 1 addition & 0 deletions docs/_data/menu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ version:
'Footnotes': '/2.x/extensions/footnotes/'
'Front Matter': '/2.x/extensions/front-matter/'
'Heading Permalinks': '/2.x/extensions/heading-permalinks/'
'Highlight': '/2.x/extensions/highlight/'
'Inlines Only': '/2.x/extensions/inlines-only/'
'Mentions': '/2.x/extensions/mentions/'
'Smart Punctuation': '/2.x/extensions/smart-punctuation/'
Expand Down
26 changes: 26 additions & 0 deletions src/Extension/Highlight/HighlightExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace League\CommonMark\Extension\Highlight;

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

class HighlightExtension implements ExtensionInterface
{
public function register(EnvironmentBuilderInterface $environment): void
{
$environment->addDelimiterProcessor(new MarkDelimiterProcessor());
$environment->addRenderer(Mark::class, new MarkRenderer());
}
}
39 changes: 39 additions & 0 deletions src/Extension/Highlight/Mark.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

declare(strict_types=1);

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace League\CommonMark\Extension\Highlight;

use League\CommonMark\Node\Inline\AbstractInline;
use League\CommonMark\Node\Inline\DelimitedInterface;

final class Mark extends AbstractInline implements DelimitedInterface
{
private string $delimiter;

public function __construct(string $delimiter = '==')
{
parent::__construct();

$this->delimiter = $delimiter;
}

public function getOpeningDelimiter(): string
{
return $this->delimiter;
}

public function getClosingDelimiter(): string
{
return $this->delimiter;
}
}
69 changes: 69 additions & 0 deletions src/Extension/Highlight/MarkDelimiterProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace League\CommonMark\Extension\Highlight;

use League\CommonMark\Delimiter\DelimiterInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Node\Inline\AbstractStringContainer;

class MarkDelimiterProcessor implements DelimiterProcessorInterface
{
public function getOpeningCharacter(): string
{
return '=';
}

public function getClosingCharacter(): string
{
return '=';
}

public function getMinLength(): int
{
return 2;
}

public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
{
if ($opener->getLength() > 2 && $closer->getLength() > 2) {
return 0;
}

if ($opener->getLength() !== $closer->getLength()) {
return 0;
}

// $opener and $closer are the same length so we just return one of them
return $opener->getLength();
}

public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void
{
$mark = new Mark(\str_repeat('=', $delimiterUse));

$next = $opener->next();
while ($next !== null && $next !== $closer) {
$tmp = $next->next();
$mark->appendChild($next);
$next = $tmp;
}

$opener->insertAfter($mark);
}

public function getCacheKey(DelimiterInterface $closer): string
{
return '=' . $closer->getLength();
}
}
50 changes: 50 additions & 0 deletions src/Extension/Highlight/MarkRenderer.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?php

declare(strict_types=1);

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <[email protected]>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace League\CommonMark\Extension\Highlight;

use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlElement;
use League\CommonMark\Xml\XmlNodeRendererInterface;

final class MarkRenderer implements NodeRendererInterface, XmlNodeRendererInterface
{
/**
* @param Mark $node
*
* {@inheritDoc}
*
* @psalm-suppress MoreSpecificImplementedParamType
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
{
Mark::assertInstanceOf($node);

return new HtmlElement('mark', $node->data->get('attributes'), $childRenderer->renderNodes($node->children()));
}

public function getXmlTagName(Node $node): string
{
return 'mark';
}

/**
* {@inheritDoc}
*/
public function getXmlAttributes(Node $node): array
{
return [];
}
}
2 changes: 2 additions & 0 deletions tests/benchmark/benchmark.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use League\CommonMark\Extension\Footnote\FootnoteExtension;
use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\Highlight\HighlightExtension;
use League\CommonMark\Extension\Mention\MentionExtension;
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
Expand Down Expand Up @@ -194,6 +195,7 @@
$environment->addExtension(new FootnoteExtension());
$environment->addExtension(new FrontMatterExtension());
$environment->addExtension(new HeadingPermalinkExtension());
$environment->addExtension(new HighlightExtension());
$environment->addExtension(new MentionExtension());
$environment->addExtension(new SmartPunctExtension());
$environment->addExtension(new StrikethroughExtension());
Expand Down
81 changes: 81 additions & 0 deletions tests/functional/Extension/Highlight/HighlightExtensionTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

declare(strict_types=1);

/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <[email protected]> and uAfrica.com (http://uafrica.com)
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace League\CommonMark\Tests\Functional\Extension\Highlight;

use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\Highlight\HighlightExtension;
use League\CommonMark\Parser\MarkdownParser;
use League\CommonMark\Renderer\HtmlRenderer;
use PHPUnit\Framework\TestCase;

final class HighlightExtensionTest extends TestCase
{
/**
* @dataProvider dataForIntegrationTest
*/
public function testMark(string $string, string $expected): void
{
$environment = new Environment();
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new HighlightExtension());

$parser = new MarkdownParser($environment);
$renderer = new HtmlRenderer($environment);

$document = $parser->parse($string);

$html = (string) $renderer->renderDocument($document);

$this->assertSame($expected, $html);
}

/**
* @return array<array<string>>
*/
public static function dataForIntegrationTest(): array
{
return [
['Hello, ==world!==', "<p>Hello, <mark>world!</mark></p>\n"],
['This is a test without any marks', "<p>This is a test without any marks</p>\n"],
['This is a test with ==valid== marks', "<p>This is a test with <mark>valid</mark> marks</p>\n"],
['This is a test `with` ==valid== marks', "<p>This is a test <code>with</code> <mark>valid</mark> marks</p>\n"],
['This is a ==unit== integration test', "<p>This is a <mark>unit</mark> integration test</p>\n"],
['==Mark== on the left', "<p><mark>Mark</mark> on the left</p>\n"],
['Mark on the ==right==', "<p>Mark on the <mark>right</mark></p>\n"],
['==Mark everywhere==', "<p><mark>Mark everywhere</mark></p>\n"],
['This ==test has no ending match', "<p>This ==test has no ending match</p>\n"],
['This ==test=== has mismatched equal signs', "<p>This ==test=== has mismatched equal signs</p>\n"],
['This ===test== also has mismatched equal signs', "<p>This ===test== also has mismatched equal signs</p>\n"],
['This one has ===three=== equal signs', "<p>This one has ===three=== equal signs</p>\n"],
["This ==has a\n\nnew paragraph==.", "<p>This ==has a</p>\n<p>new paragraph==.</p>\n"],
['Hello == == world', "<p>Hello == == world</p>\n"],
['This **is ==a little** test of mismatched delimiters==', "<p>This <strong>is ==a little</strong> test of mismatched delimiters==</p>\n"],
['Из: твоя ==тест== ветка', "<p>Из: твоя <mark>тест</mark> ветка</p>\n"],
['This one combines ==nested ==mark== text==', "<p>This one combines <mark>nested <mark>mark</mark> text</mark></p>\n"],
['Here we have **emphasized text containing a ==mark==**', "<p>Here we have <strong>emphasized text containing a <mark>mark</mark></strong></p>\n"],
['Four trailing equal signs ====', "<p>Four trailing equal signs ====</p>\n"],
['==Unmatched left', "<p>==Unmatched left</p>\n"],
['Unmatched right==', "<p>Unmatched right==</p>\n"],
['==foo=bar==', "<p><mark>foo=bar</mark></p>\n"],
['==foo==bar==', "<p><mark>foo</mark>bar==</p>\n"],
['==foo===bar==', "<p><mark>foo===bar</mark></p>\n"],
['==foo====bar==', "<p><mark>foo====bar</mark></p>\n"],
['==foo=====bar==', "<p><mark>foo=====bar</mark></p>\n"],
['==foo======bar==', "<p><mark>foo======bar</mark></p>\n"],
['==foo=======bar==', "<p><mark>foo=======bar</mark></p>\n"],
['> inside a ==blockquote==', "<blockquote>\n<p>inside a <mark>blockquote</mark></p>\n</blockquote>\n"],
];
}
}
Loading