Skip to content

Commit

Permalink
Import the Attributes extension (#484)
Browse files Browse the repository at this point in the history
This extension is based https://github.com/webuni/commonmark-attributes-extension,
imported and relicensed with permission from the maintainer:
#474 (comment)
  • Loading branch information
colinodell committed May 25, 2020
1 parent fb52dd0 commit 5326e04
Show file tree
Hide file tree
Showing 24 changed files with 796 additions and 0 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi

### Added

- Added new `AttributesExtension` based on <https://github.com/webuni/commonmark-attributes-extension> (#474)
- Added new `FootnoteExtension` based on <https://github.com/rezozero/commonmark-ext-footnotes> (#474)
- Added a new `MentionParser` to replace `InlineMentionParser` with more flexibility and customization
- Added the ability to render `TableOfContents` nodes anywhere in a document (given by a placeholder)
Expand Down
57 changes: 57 additions & 0 deletions docs/1.5/extensions/attributes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
layout: default
title: Attributes Extension
description: The AttributesExtension allows HTML attributes to be added from within the document.
---

# Attributes

The `AttributesExtension` allows HTML attributes to be added from within the document.

## Attribute Syntax

The basic syntax was inspired by [Kramdown](http://kramdown.gettalong.org/syntax.html#attribute-list-definitions)'s Attribute Lists feature.

You can assign any attribute to a block-level element. Just directly prepend or follow the block with a block inline attribute list.
That consists of a left curly brace, optionally followed by a colon, the attribute definitions and a right curly brace:

```markdown
> A nice blockquote
{: title="Blockquote title"}

{#id .class}
## Header
```

As with a block-level element you can assign any attribute to a span-level elements using a span inline attribute list,
that has the same syntax and must immediately follow the span-level element:

```markdown
This is *red*{style="color: red"}.
```

## Usage

Configure your `Environment` as usual and simply add the `AttributesExtension`:

```php
<?php
use League\CommonMark\CommonMarkConverter;
use League\CommonMark\Environment;
use League\CommonMark\Extension\Attributes\AttributesExtension;

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

// Add the extension
$environment->addExtension(new AttributesExtension());

// Set your configuration if needed
$config = [
// ...
];

// Instantiate the converter engine and start converting some Markdown!
$converter = new CommonMarkConverter($config, $environment);
echo $converter->convertToHtml('# Hello World!');
```
1 change: 1 addition & 0 deletions docs/1.5/extensions/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ These extensions are not part of GFM, but can be useful in many cases:

| Extension | Purpose | Documentation |
| --------- | ------- | ------------- |
| `AttributesExtension` | Add HTML attributes (like `id` and `class`) from within the Markdown content | [Documentation](/1.5/extensions/attributes/) |
| `ExternalLinkExtension` | Tags external links with additional markup | [Documentation](/1.5/extensions/external-links/) |
| `FootnoteExtension` | Add footnote references throughout the document and show a listing of them at the bottom | [Documentation](/1.5/extensions/footnotes/) |
| `HeadingPermalinkExtension` | Makes heading elements linkable | [Documentation](/1.5/extensions/heading-permalinks/) |
Expand Down
1 change: 1 addition & 0 deletions docs/_data/menu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ version:
'Overview': '/1.5/extensions/overview/'
'CommonMark': '/1.5/extensions/commonmark/'
'Github-Flavored Markdown': '/1.5/extensions/github-flavored-markdown/'
'Attributes': '/1.5/extensions/attributes/'
'Autolinks': '/1.5/extensions/autolinks/'
'Disallowed Raw HTML': '/1.5/extensions/disallowed-raw-html/'
'External Links': '/1.5/extensions/external-links/'
Expand Down
32 changes: 32 additions & 0 deletions src/Extension/Attributes/AttributesExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

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

declare(strict_types=1);

namespace League\CommonMark\Extension\Attributes;

use League\CommonMark\ConfigurableEnvironmentInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Attributes\Event\AttributesListener;
use League\CommonMark\Extension\Attributes\Parser\AttributesBlockParser;
use League\CommonMark\Extension\Attributes\Parser\AttributesInlineParser;
use League\CommonMark\Extension\ExtensionInterface;

final class AttributesExtension implements ExtensionInterface
{
public function register(ConfigurableEnvironmentInterface $environment)
{
$environment->addBlockParser(new AttributesBlockParser());
$environment->addInlineParser(new AttributesInlineParser());
$environment->addEventListener(DocumentParsedEvent::class, [new AttributesListener(), 'processDocument']);
}
}
176 changes: 176 additions & 0 deletions src/Extension/Attributes/Event/AttributesListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<?php

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

declare(strict_types=1);

namespace League\CommonMark\Extension\Attributes\Event;

use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Block\Element\Document;
use League\CommonMark\Block\Element\FencedCode;
use League\CommonMark\Block\Element\ListBlock;
use League\CommonMark\Block\Element\ListItem;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\Attributes\Node\Attributes;
use League\CommonMark\Extension\Attributes\Node\AttributesInline;
use League\CommonMark\Inline\Element\AbstractInline;
use League\CommonMark\Node\Node;

final class AttributesListener
{
private const DIRECTION_PREFIX = 'prefix';
private const DIRECTION_SUFFIX = 'suffix';

public function processDocument(DocumentParsedEvent $event): void
{
$walker = $event->getDocument()->walker();
while ($event = $walker->next()) {
$node = $event->getNode();
if (!$node instanceof AttributesInline && ($event->isEntering() || !$node instanceof Attributes)) {
continue;
}

[$target, $direction] = self::findTargetAndDirection($node);

if ($target instanceof Node) {
$parent = $target->parent();
if ($parent instanceof ListItem && $parent->parent() instanceof ListBlock && $parent->parent()->isTight()) {
$target = $parent;
}

if ($direction === self::DIRECTION_SUFFIX) {
$attributes = self::merge($target, $node->getAttributes());
} else {
$attributes = self::merge($node->getAttributes(), $target);
}

if ($target instanceof AbstractBlock || $target instanceof AbstractInline) {
$target->data['attributes'] = $attributes;
}
}

if ($node instanceof AbstractBlock && $node->endsWithBlankLine() && $node->next() && $node->previous()) {
$previous = $node->previous();
if ($previous instanceof AbstractBlock) {
$previous->setLastLineBlank(true);
}
}

$node->detach();
}
}

/**
* @param Node $node
*
* @return array<Node|string>
*/
private static function findTargetAndDirection(Node $node): array
{
$target = null;
$direction = null;
$previous = $next = $node;
while (true) {
$previous = self::getPrevious($previous);
$next = self::getNext($next);

if ($previous === null && $next === null) {
if (!$node->parent() instanceof FencedCode) {
$target = $node->parent();
$direction = self::DIRECTION_SUFFIX;
}

break;
}

if ($node instanceof AttributesInline && ($previous === null || ($previous instanceof AbstractInline && $node->isBlock()))) {
continue;
}

if ($previous !== null && !self::isAttributesNode($previous)) {
$target = $previous;
$direction = self::DIRECTION_SUFFIX;

break;
}

if ($next !== null && !self::isAttributesNode($next)) {
$target = $next;
$direction = self::DIRECTION_PREFIX;

break;
}
}

return [$target, $direction];
}

private static function getPrevious(?Node $node = null): ?Node
{
$previous = $node instanceof Node ? $node->previous() : null;

if ($previous instanceof AbstractBlock && $previous->endsWithBlankLine()) {
$previous = null;
}

return $previous;
}

private static function getNext(?Node $node = null): ?Node
{
$next = $node instanceof Node ? $node->next() : null;

if ($node instanceof AbstractBlock && $node->endsWithBlankLine()) {
$next = null;
}

return $next;
}

private static function isAttributesNode(Node $node): bool
{
return $node instanceof Attributes || $node instanceof AttributesInline;
}

/**
* @param AbstractBlock|AbstractInline|array<string, mixed> $attributes1
* @param AbstractBlock|AbstractInline|array<string, mixed> $attributes2
*
* @return array<string, mixed>
*/
private static function merge($attributes1, $attributes2): array
{
$attributes = [];
foreach ([$attributes1, $attributes2] as $arg) {
if ($arg instanceof AbstractBlock || $arg instanceof AbstractInline) {
$arg = $arg->data['attributes'] ?? [];
}

$arg = (array) $arg;
if (isset($arg['class'])) {
foreach (\array_filter(\explode(' ', \trim($arg['class']))) as $class) {
$attributes['class'][] = $class;
}

unset($arg['class']);
}

$attributes = \array_merge($attributes, $arg);
}

if (isset($attributes['class'])) {
$attributes['class'] = \implode(' ', $attributes['class']);
}

return $attributes;
}
}
62 changes: 62 additions & 0 deletions src/Extension/Attributes/Node/Attributes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<?php

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

declare(strict_types=1);

namespace League\CommonMark\Extension\Attributes\Node;

use League\CommonMark\Block\Element\AbstractBlock;
use League\CommonMark\Cursor;

final class Attributes extends AbstractBlock
{
/** @var array<string, mixed> */
private $attributes;

/**
* @param array<string, mixed> $attributes
*/
public function __construct(array $attributes)
{
$this->attributes = $attributes;
}

/**
* @return array<string, mixed>
*/
public function getAttributes(): array
{
return $this->attributes;
}

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

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

public function matchesNextLine(Cursor $cursor): bool
{
$this->setLastLineBlank($cursor->isBlank());

return false;
}

public function shouldLastLineBeBlank(Cursor $cursor, int $currentLineNumber): bool
{
return false;
}
}
Loading

0 comments on commit 5326e04

Please sign in to comment.