Mention Extension

The MentionExtension makes it easy to parse shortened mentions and references like @colinodell to a Twitter URL or #123 to a GitHub issue URL. You can create your own custom syntax by defining which symbol you want to use and how to generate the corresponding URL.

Usage

You can create your own custom syntax by supplying the configuration with an array of options that define the starting symbol, a regular expression to match against, and any custom URL template or callable to generate the URL.

<?php
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Mention\MentionExtension;

// Obtain a pre-configured Environment with all the CommonMark parsers/renderers ready-to-go.
$environment = Environment::createCommonMarkEnvironment();

// Add the Mention extension.
$environment->addExtension(new MentionExtension());

// Set your configuration.
$config = [
    'mentions' => [
        // GitHub handler mention configuration.
        // Sample Input:  `@colinodell`
        // Sample Output: `<a href="https://www.github.com/colinodell">@colinodell</a>`
        'github_handle' => [
            'symbol'    => '@',
            'regex'     => '/^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/',
            'generator' => 'https://github.com/%s',
        ],
        // GitHub issue mention configuration.
        // Sample Input:  `#473`
        // Sample Output: `<a href="https://github.com/thephpleague/commonmark/issues/473">#473</a>`
        'github_issue' => [
            'symbol'    => '#',
            'regex'     => '/^\d+/',
            'generator' => "https://github.com/thephpleague/commonmark/issues/%d",
        ],
        // Twitter handler mention configuration.
        // Sample Input:  `@colinodell`
        // Sample Output: `<a href="https://www.twitter.com/colinodell">@colinodell</a>`
        // Note: when registering more than one mention parser with the same symbol, the last one registered will
        // always take precedence.
        'twitter_handle' => [
            'symbol'    => '@',
            'regex'     => '/^[A-Za-z0-9_]{1,15}(?!\w)/',
            'generator' => 'https://twitter.com/%s',
        ],
    ],
];

// Instantiate the converter engine and start converting some Markdown!
$converter = new CommonMarkConverter($config, $environment);
echo $converter->convertToHtml('Follow me on Twitter: @colinodell');
// Output:
// <p>Follow me on Twitter: <a href="https://twitter.com/colinodell">@colinodell</a></p>

String-Based URL Templates

URL templates are perfect for situations where the identifier is inserted directly into a URL:

"@colinodell" => https://www.twitter.com/colinodell
 ^      ^                                    ^
 |       \________ Identifier ______________/
Symbol

Examples of using string-based URL templates can be seen in the usage example above - you simply provide a string to the generator option.

Note that the URL template must be a string, and that the %s placeholder will be replaced by whatever the user enters after the symbol (in this case, @). You can use any symbol, regex pattern, or URL template you want!

Custom Callback-Based Parsers

Need more power than simply adding the mention inside a string based URL template? The MentionExtension automatically detects if the provided generator is an object that implements \League\CommonMark\Extension\Mention\Generator\MentionGeneratorInterface or a valid PHP callable that can generate a resulting URL.

use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Mention\Generator\MentionGeneratorInterface;
use League\CommonMark\Extension\Mention\Mention;
use League\CommonMark\Extension\Mention\MentionExtension;
use League\CommonMark\Node\Inline\AbstractInline;

// Obtain a pre-configured Environment with all the CommonMark parsers/renderers ready-to-go.
$environment = Environment::createCommonMarkEnvironment();

// Add the Mention extension.
$environment->addExtension(new MentionExtension());

// Set your configuration.
$config = [
    'mentions' => [
        'github_handle' => [
            'symbol'    => '@',
            'regex'     => '/^[a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38}(?!\w)/',
            // The recommended approach is to provide a class that implements MentionGeneratorInterface.
            'generator' => new GithubUserMentionGenerator(), // TODO: Implement such a class yourself
        ],
        'github_issue' => [
            'symbol'    => '#',
            'regex'     => '/^\d+/',
            // Alternatively, if your logic is simple, you can implement an inline anonymous class like this example.
            'generator' => new class implements MentionGeneratorInterface {
                 public function generateMention(Mention $mention): ?AbstractInline
                 {
                     $mention->setUrl(\sprintf('https://github.com/thephpleague/commonmark/issues/%d', $mention->getIdentifier()));

                     return $mention;
                 }
             },
        ],
        'github_issue' => [
            'symbol'    => '#',
            'regex'     => '/^\d+/',
            // Any type of callable, including anonymous closures, (with optional typehints) are also supported.
            // This allows for better compatibility between different major versions of CommonMark.
            // However, you sacrifice the ability to type-check which means automated development tools
            // may not notice if your code is no longer compatible with new versions - you'll need to
            // manually verify this yourself.
            'generator' => function ($mention) {
                // Immediately return if not passed the supported Mention object.
                // This is an example of the types of manual checks you'll need to perform if not using type hints
                if (!($mention instanceof Mention)) {
                    return null;
                }

                $mention->setUrl(\sprintf('https://github.com/thephpleague/commonmark/issues/%d', $mention->getIdentifier()));

                return $mention;
            },
        ],

    ],
];

// Instantiate the converter engine and start converting some Markdown!
$converter = new CommonMarkConverter($config, $environment);
echo $converter->convertToHtml('Follow me on Twitter: @colinodell');
// Output:
// <p>Follow me on Twitter: <a href="https://www.github.com/colinodell">@colinodell</a></p>

When implementing MentionGeneratorInterface or a simple callable, you’ll receive a single Mention parameter and must either:

Here’s a faux-real-world example of how you might use such a generator for your application. Imagine you want to parse @username into custom user profile links for your application, but only if the user exists. You could create a class like the following which integrates with the framework your application is built on:

class UserMentionGenerator implements MentionGeneratorInterface
{
    private $currentUser;
    private $userRepository;
    private $router;

    public function __construct (AccountInterface $currentUser, UserRepository $userRepository, Router $router)
    {
        $this->currentUser = $currentUser;
        $this->userRepository = $userRepository;
        $this->router = $router;
    }

    public function generateMention(Mention $mention): ?AbstractInline
    {
        // Determine mention visibility (i.e. member privacy).
        if (!$this->currentUser->hasPermission('access profiles')) {
            $emphasis = new \League\CommonMark\Inline\Element\Emphasis();
            $emphasis->appendChild(new \League\CommonMark\Inline\Element\Text('[members only]'));
            return $emphasis;
        }

        // Locate the user that is mentioned.
        $user = $this->userRepository->findUser($mention->getIdentifier());

        // The mention isn't valid if the user does not exist.
        if (!$user) {
            return null;
        }

        // Change the label.
        $mention->setLabel($user->getFullName());
        // Use the path to their profile as the URL, typecasting to a string in case the service returns
        // a __toString object; otherwise you will need to figure out a way to extract the string URL
        // from the service.
        $mention->setUrl((string) $this->router->generate('user_profile', ['id' => $user->getId()]));

        return $mention;
    }
}

You can then hook this class up to a mention definition in the configuration to generate profile URLs from Markdown mentions:

<?php
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\Mention\MentionExtension;

// Grab your UserMentionGenerator somehow, perhaps from a DI container or instantiate it if needed
$userMentionGenerator = $container->get(UserMentionGenerator::class);

// Obtain a pre-configured Environment with all the CommonMark parsers/renderers ready-to-go
$environment = Environment::createCommonMarkEnvironment();

// Add the Mention extension.
$environment->addExtension(new MentionExtension());

// Set your configuration.
$config = [
    'mentions' => [
        'user_url_generator' => [
            'symbol'    => '@',
            'regex'     => '/^[a-z0-9]+/i',
            'generator' => $userMentionGenerator,
        ],
    ],
];

// Instantiate the converter engine and start converting some Markdown!
$converter = new CommonMarkConverter($config, $environment);
echo $converter->convertToHtml('You should ask @colinodell about that');

// Output (if current user has permission to view profiles):
// <p>You should ask <a href="/user/123/profile">Colin O'Dell</a> about that</p>
//
// Output (if current user doesn't have has access to view profiles):
// <p>You should ask <em>[members only]</em> about that</p>

Rendering

Whenever a mention is found, a Mention object is added to the document’s AST. This object extends from Link, so it’ll be rendered as a normal <a> tag by default.

If you need more control over the output you can implement a custom renderer for the Mention type and convert it to whatever HTML you wish!