Skip to content

Commit b703d32

Browse files
authored
Merge pull request #1100 from ossobuffo/main
Add Mark (highlight) extension
2 parents fdd5ef4 + 9fad6b6 commit b703d32

File tree

15 files changed

+477
-0
lines changed

15 files changed

+477
-0
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
66

77
## [Unreleased][unreleased]
88

9+
### Added
10+
- Added a new `HighlightExtension` for marking important text using `==` syntax (#1100)
11+
912
## [2.7.0]
1013

1114
This is a **security release** to address a potential cross-site scripting (XSS) vulnerability when using the `AttributesExtension` with untrusted user input.

docs/2.x/extensions/highlight.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
---
2+
layout: default
3+
title: Highlight Extension
4+
description: The HighlightExtension allows marking important text.
5+
redirect_from:
6+
- /extensions/highlight/
7+
---
8+
9+
# Highlight Extension
10+
11+
This extension adds support for highlighting important text using the `==` syntax. For example, the Markdown:
12+
13+
```markdown
14+
I need to highlight these ==very important words==.
15+
```
16+
17+
Would be rendered to HTML as:
18+
19+
```html
20+
<p>I need to highlight these <mark>very important words</mark>.</p>
21+
```
22+
23+
Which could then be styled using CSS to produce a highlighter effect.
24+
25+
## Installation
26+
27+
This extension is bundled with `league/commonmark`. This library can be installed via Composer:
28+
29+
```bash
30+
composer require league/commonmark
31+
```
32+
33+
See the [installation](/2.x/installation/) section for more details.
34+
35+
## Usage
36+
37+
This extension can be added to any new `Environment`:
38+
39+
```php
40+
use League\CommonMark\Environment\Environment;
41+
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
42+
use League\CommonMark\Extension\Highlight\HighlightExtension;
43+
use League\CommonMark\MarkdownConverter;
44+
45+
// Define your configuration, if needed
46+
$config = [];
47+
48+
// Configure the Environment with all the CommonMark parsers/renderers
49+
$environment = new Environment($config);
50+
$environment->addExtension(new CommonMarkCoreExtension());
51+
52+
// Add this extension
53+
$environment->addExtension(new HighlightExtension());
54+
55+
// Instantiate the converter engine and start converting some Markdown!
56+
$converter = new MarkdownConverter($environment);
57+
echo $converter->convert('I need to highlight these ==very important words==.');
58+
```

docs/2.x/extensions/overview.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ to enhance your experience out-of-the-box depending on your specific use-cases.
3838
| [Front Matter] | Parses YAML front matter from your Markdown input | `2.0.0` | |
3939
| **[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` | |
4040
| [Heading Permalinks] | Makes heading elements linkable | `1.4.0` | |
41+
| [Highlight] | Mark text as being highlighted for reference or notation purposes | `2.8.0` | |
4142
| [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` | |
4243
| [Mentions] | Easy parsing of `@mention` and `#123`-style references | `1.5.0` | |
4344
| [Strikethrough] | Allows using tilde characters (`~~`) for ~strikethrough~ formatting | `1.3.0` | <i class="fab fa-github"></i> |
@@ -119,6 +120,7 @@ See the [Custom Extensions](/2.x/customization/extensions/) page for details on
119120
[Front Matter]: /2.x/extensions/front-matter/
120121
[GitHub Flavored Markdown]: /2.x/extensions/github-flavored-markdown/
121122
[Heading Permalinks]: /2.x/extensions/heading-permalinks/
123+
[Highlight]: /2.x/extensions/highlight/
122124
[Inlines Only]: /2.x/extensions/inlines-only/
123125
[Mentions]: /2.x/extensions/mentions/
124126
[Strikethrough]: /2.x/extensions/strikethrough/

docs/_data/menu.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ version:
2525
'Footnotes': '/2.x/extensions/footnotes/'
2626
'Front Matter': '/2.x/extensions/front-matter/'
2727
'Heading Permalinks': '/2.x/extensions/heading-permalinks/'
28+
'Highlight': '/2.x/extensions/highlight/'
2829
'Inlines Only': '/2.x/extensions/inlines-only/'
2930
'Mentions': '/2.x/extensions/mentions/'
3031
'Smart Punctuation': '/2.x/extensions/smart-punctuation/'
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the league/commonmark package.
7+
*
8+
* (c) Colin O'Dell <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace League\CommonMark\Extension\Highlight;
15+
16+
use League\CommonMark\Environment\EnvironmentBuilderInterface;
17+
use League\CommonMark\Extension\ExtensionInterface;
18+
19+
class HighlightExtension implements ExtensionInterface
20+
{
21+
public function register(EnvironmentBuilderInterface $environment): void
22+
{
23+
$environment->addDelimiterProcessor(new MarkDelimiterProcessor());
24+
$environment->addRenderer(Mark::class, new MarkRenderer());
25+
}
26+
}

src/Extension/Highlight/Mark.php

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the league/commonmark package.
7+
*
8+
* (c) Colin O'Dell <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace League\CommonMark\Extension\Highlight;
15+
16+
use League\CommonMark\Node\Inline\AbstractInline;
17+
use League\CommonMark\Node\Inline\DelimitedInterface;
18+
19+
final class Mark extends AbstractInline implements DelimitedInterface
20+
{
21+
private string $delimiter;
22+
23+
public function __construct(string $delimiter = '==')
24+
{
25+
parent::__construct();
26+
27+
$this->delimiter = $delimiter;
28+
}
29+
30+
public function getOpeningDelimiter(): string
31+
{
32+
return $this->delimiter;
33+
}
34+
35+
public function getClosingDelimiter(): string
36+
{
37+
return $this->delimiter;
38+
}
39+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the league/commonmark package.
7+
*
8+
* (c) Colin O'Dell <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace League\CommonMark\Extension\Highlight;
15+
16+
use League\CommonMark\Delimiter\DelimiterInterface;
17+
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
18+
use League\CommonMark\Node\Inline\AbstractStringContainer;
19+
20+
class MarkDelimiterProcessor implements DelimiterProcessorInterface
21+
{
22+
public function getOpeningCharacter(): string
23+
{
24+
return '=';
25+
}
26+
27+
public function getClosingCharacter(): string
28+
{
29+
return '=';
30+
}
31+
32+
public function getMinLength(): int
33+
{
34+
return 2;
35+
}
36+
37+
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
38+
{
39+
if ($opener->getLength() > 2 && $closer->getLength() > 2) {
40+
return 0;
41+
}
42+
43+
if ($opener->getLength() !== $closer->getLength()) {
44+
return 0;
45+
}
46+
47+
// $opener and $closer are the same length so we just return one of them
48+
return $opener->getLength();
49+
}
50+
51+
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void
52+
{
53+
$mark = new Mark(\str_repeat('=', $delimiterUse));
54+
55+
$next = $opener->next();
56+
while ($next !== null && $next !== $closer) {
57+
$tmp = $next->next();
58+
$mark->appendChild($next);
59+
$next = $tmp;
60+
}
61+
62+
$opener->insertAfter($mark);
63+
}
64+
65+
public function getCacheKey(DelimiterInterface $closer): string
66+
{
67+
return '=' . $closer->getLength();
68+
}
69+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the league/commonmark package.
7+
*
8+
* (c) Colin O'Dell <[email protected]>
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace League\CommonMark\Extension\Highlight;
15+
16+
use League\CommonMark\Node\Node;
17+
use League\CommonMark\Renderer\ChildNodeRendererInterface;
18+
use League\CommonMark\Renderer\NodeRendererInterface;
19+
use League\CommonMark\Util\HtmlElement;
20+
use League\CommonMark\Xml\XmlNodeRendererInterface;
21+
22+
final class MarkRenderer implements NodeRendererInterface, XmlNodeRendererInterface
23+
{
24+
/**
25+
* @param Mark $node
26+
*
27+
* {@inheritDoc}
28+
*
29+
* @psalm-suppress MoreSpecificImplementedParamType
30+
*/
31+
public function render(Node $node, ChildNodeRendererInterface $childRenderer): \Stringable
32+
{
33+
Mark::assertInstanceOf($node);
34+
35+
return new HtmlElement('mark', $node->data->get('attributes'), $childRenderer->renderNodes($node->children()));
36+
}
37+
38+
public function getXmlTagName(Node $node): string
39+
{
40+
return 'mark';
41+
}
42+
43+
/**
44+
* {@inheritDoc}
45+
*/
46+
public function getXmlAttributes(Node $node): array
47+
{
48+
return [];
49+
}
50+
}

tests/benchmark/benchmark.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use League\CommonMark\Extension\Footnote\FootnoteExtension;
2525
use League\CommonMark\Extension\FrontMatter\FrontMatterExtension;
2626
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
27+
use League\CommonMark\Extension\Highlight\HighlightExtension;
2728
use League\CommonMark\Extension\Mention\MentionExtension;
2829
use League\CommonMark\Extension\SmartPunct\SmartPunctExtension;
2930
use League\CommonMark\Extension\Strikethrough\StrikethroughExtension;
@@ -194,6 +195,7 @@
194195
$environment->addExtension(new FootnoteExtension());
195196
$environment->addExtension(new FrontMatterExtension());
196197
$environment->addExtension(new HeadingPermalinkExtension());
198+
$environment->addExtension(new HighlightExtension());
197199
$environment->addExtension(new MentionExtension());
198200
$environment->addExtension(new SmartPunctExtension());
199201
$environment->addExtension(new StrikethroughExtension());
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/*
6+
* This file is part of the league/commonmark package.
7+
*
8+
* (c) Colin O'Dell <[email protected]> and uAfrica.com (http://uafrica.com)
9+
*
10+
* For the full copyright and license information, please view the LICENSE
11+
* file that was distributed with this source code.
12+
*/
13+
14+
namespace League\CommonMark\Tests\Functional\Extension\Highlight;
15+
16+
use League\CommonMark\Environment\Environment;
17+
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
18+
use League\CommonMark\Extension\Highlight\HighlightExtension;
19+
use League\CommonMark\Parser\MarkdownParser;
20+
use League\CommonMark\Renderer\HtmlRenderer;
21+
use PHPUnit\Framework\TestCase;
22+
23+
final class HighlightExtensionTest extends TestCase
24+
{
25+
/**
26+
* @dataProvider dataForIntegrationTest
27+
*/
28+
public function testMark(string $string, string $expected): void
29+
{
30+
$environment = new Environment();
31+
$environment->addExtension(new CommonMarkCoreExtension());
32+
$environment->addExtension(new HighlightExtension());
33+
34+
$parser = new MarkdownParser($environment);
35+
$renderer = new HtmlRenderer($environment);
36+
37+
$document = $parser->parse($string);
38+
39+
$html = (string) $renderer->renderDocument($document);
40+
41+
$this->assertSame($expected, $html);
42+
}
43+
44+
/**
45+
* @return array<array<string>>
46+
*/
47+
public static function dataForIntegrationTest(): array
48+
{
49+
return [
50+
['Hello, ==world!==', "<p>Hello, <mark>world!</mark></p>\n"],
51+
['This is a test without any marks', "<p>This is a test without any marks</p>\n"],
52+
['This is a test with ==valid== marks', "<p>This is a test with <mark>valid</mark> marks</p>\n"],
53+
['This is a test `with` ==valid== marks', "<p>This is a test <code>with</code> <mark>valid</mark> marks</p>\n"],
54+
['This is a ==unit== integration test', "<p>This is a <mark>unit</mark> integration test</p>\n"],
55+
['==Mark== on the left', "<p><mark>Mark</mark> on the left</p>\n"],
56+
['Mark on the ==right==', "<p>Mark on the <mark>right</mark></p>\n"],
57+
['==Mark everywhere==', "<p><mark>Mark everywhere</mark></p>\n"],
58+
['This ==test has no ending match', "<p>This ==test has no ending match</p>\n"],
59+
['This ==test=== has mismatched equal signs', "<p>This ==test=== has mismatched equal signs</p>\n"],
60+
['This ===test== also has mismatched equal signs', "<p>This ===test== also has mismatched equal signs</p>\n"],
61+
['This one has ===three=== equal signs', "<p>This one has ===three=== equal signs</p>\n"],
62+
["This ==has a\n\nnew paragraph==.", "<p>This ==has a</p>\n<p>new paragraph==.</p>\n"],
63+
['Hello == == world', "<p>Hello == == world</p>\n"],
64+
['This **is ==a little** test of mismatched delimiters==', "<p>This <strong>is ==a little</strong> test of mismatched delimiters==</p>\n"],
65+
['Из: твоя ==тест== ветка', "<p>Из: твоя <mark>тест</mark> ветка</p>\n"],
66+
['This one combines ==nested ==mark== text==', "<p>This one combines <mark>nested <mark>mark</mark> text</mark></p>\n"],
67+
['Here we have **emphasized text containing a ==mark==**', "<p>Here we have <strong>emphasized text containing a <mark>mark</mark></strong></p>\n"],
68+
['Four trailing equal signs ====', "<p>Four trailing equal signs ====</p>\n"],
69+
['==Unmatched left', "<p>==Unmatched left</p>\n"],
70+
['Unmatched right==', "<p>Unmatched right==</p>\n"],
71+
['==foo=bar==', "<p><mark>foo=bar</mark></p>\n"],
72+
['==foo==bar==', "<p><mark>foo</mark>bar==</p>\n"],
73+
['==foo===bar==', "<p><mark>foo===bar</mark></p>\n"],
74+
['==foo====bar==', "<p><mark>foo====bar</mark></p>\n"],
75+
['==foo=====bar==', "<p><mark>foo=====bar</mark></p>\n"],
76+
['==foo======bar==', "<p><mark>foo======bar</mark></p>\n"],
77+
['==foo=======bar==', "<p><mark>foo=======bar</mark></p>\n"],
78+
['> inside a ==blockquote==', "<blockquote>\n<p>inside a <mark>blockquote</mark></p>\n</blockquote>\n"],
79+
];
80+
}
81+
}

0 commit comments

Comments
 (0)