Skip to content

Commit

Permalink
Merge pull request #787 from thephpleague/decorate-table-output
Browse files Browse the repository at this point in the history
Decorate table output
  • Loading branch information
colinodell committed Jan 22, 2022
2 parents 057518e + 01984fa commit 00c1b72
Show file tree
Hide file tree
Showing 10 changed files with 197 additions and 56 deletions.
5 changes: 5 additions & 0 deletions .phpstorm.meta.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@
'slug_normalizer/instance',
'slug_normalizer/max_length',
'slug_normalizer/unique',
'table',
'table/wrap',
'table/wrap/attributes',
'table/wrap/enabled',
'table/wrap/tag',
'table_of_contents',
'table_of_contents/html_class',
'table_of_contents/max_heading_level',
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi

- Added new `ConverterInterface`
- Added new `MarkdownToXmlConverter` class
- Added new `HtmlDecorator` class which can wrap existing renderers with additional HTML tags
- Added new `table/wrap` config to apply an optional wrapping/container element around a table (#780)

### Changed

- `HtmlElement` contents can now consist of any `Stringable`, not just `HtmlElement` and `string`

### Deprecated

Expand Down
34 changes: 33 additions & 1 deletion docs/2.2/extensions/tables.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,15 @@ use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\MarkdownConverter;

// Define your configuration, if needed
$config = [];
$config = [
'table' => [
'wrap' => [
'enabled' => false,
'tag' => 'div',
'attributes' => [],
],
],
];

// Configure the Environment with all the CommonMark parsers/renderers
$environment = new Environment($config);
Expand Down Expand Up @@ -89,6 +97,30 @@ Result:
| cell 2.1 | cell 2.2 | cell 2.3 |
```

## Configuration

### Wrapping Container

You can "wrap" the table with a container element by configuring the following options:

- `enabled`: (`boolean`) Whether to wrap the table with a container element. Defaults to `false`.
- `tag`: (`string`) The tag name of the container element. Defaults to `div`.
- `attributes`: (`array`) An array of attributes to apply to the container element. Defaults to `[]`.

For example, to wrap all tables within a `<div class="table-responsive">` container element:

```php
$config = [
'table' => [
'wrap' => [
'enabled' => true,
'tag' => 'div',
'attributes' => ['class' => 'table-responsive'],
],
],
];
```

## Credits

The Table functionality was originally built by [Martin Hasoň](https://github.com/hason) and [Webuni s.r.o.](https://www.webuni.cz) before it was merged into the core parser.
25 changes: 22 additions & 3 deletions src/Extension/Table/TableExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,35 @@
namespace League\CommonMark\Extension\Table;

use League\CommonMark\Environment\EnvironmentBuilderInterface;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Extension\ConfigurableExtensionInterface;
use League\CommonMark\Renderer\HtmlDecorator;
use League\Config\ConfigurationBuilderInterface;
use Nette\Schema\Expect;

final class TableExtension implements ExtensionInterface
final class TableExtension implements ConfigurableExtensionInterface
{
public function configureSchema(ConfigurationBuilderInterface $builder): void
{
$builder->addSchema('table', Expect::structure([
'wrap' => Expect::structure([
'enabled' => Expect::bool()->default(false),
'tag' => Expect::string()->default('div'),
'attributes' => Expect::arrayOf(Expect::string()),
]),
]));
}

public function register(EnvironmentBuilderInterface $environment): void
{
$tableRenderer = new TableRenderer();
if ($environment->getConfiguration()->get('table/wrap/enabled')) {
$tableRenderer = new HtmlDecorator($tableRenderer, $environment->getConfiguration()->get('table/wrap/tag'), $environment->getConfiguration()->get('table/wrap/attributes'));
}

$environment
->addBlockStartParser(new TableStartParser())

->addRenderer(Table::class, new TableRenderer())
->addRenderer(Table::class, $tableRenderer)
->addRenderer(TableSection::class, new TableSectionRenderer())
->addRenderer(TableRow::class, new TableRowRenderer())
->addRenderer(TableCell::class, new TableCellRenderer());
Expand Down
45 changes: 45 additions & 0 deletions src/Renderer/HtmlDecorator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

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

namespace League\CommonMark\Renderer;

use League\CommonMark\Node\Node;
use League\CommonMark\Util\HtmlElement;

final class HtmlDecorator implements NodeRendererInterface
{
private NodeRendererInterface $inner;
private string $tag;
/** @var array<string, string|string[]|bool> */
private array $attributes;
private bool $selfClosing;

/**
* @param array<string, string|string[]|bool> $attributes
*/
public function __construct(NodeRendererInterface $inner, string $tag, array $attributes = [], bool $selfClosing = false)
{
$this->inner = $inner;
$this->tag = $tag;
$this->attributes = $attributes;
$this->selfClosing = $selfClosing;
}

/**
* {@inheritDoc}
*/
public function render(Node $node, ChildNodeRendererInterface $childRenderer)
{
return new HtmlElement($this->tag, $this->attributes, $this->inner->render($node, $childRenderer), $this->selfClosing);
}
}
8 changes: 4 additions & 4 deletions src/Util/HtmlElement.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ final class HtmlElement implements \Stringable
/** @var array<string, string|bool> */
private array $attributes = [];

/** @var HtmlElement|HtmlElement[]|string */
/** @var \Stringable|\Stringable[]|string */
private $contents;

/** @psalm-readonly */
Expand All @@ -33,7 +33,7 @@ final class HtmlElement implements \Stringable
/**
* @param string $tagName Name of the HTML tag
* @param array<string, string|string[]|bool> $attributes Array of attributes (values should be unescaped)
* @param HtmlElement|HtmlElement[]|string|null $contents Inner contents, pre-escaped if needed
* @param \Stringable|\Stringable[]|string|null $contents Inner contents, pre-escaped if needed
* @param bool $selfClosing Whether the tag is self-closing
*/
public function __construct(string $tagName, array $attributes = [], $contents = '', bool $selfClosing = false)
Expand Down Expand Up @@ -89,7 +89,7 @@ public function setAttribute(string $key, $value): self
}

/**
* @return HtmlElement|HtmlElement[]|string
* @return \Stringable|\Stringable[]|string
*
* @psalm-immutable
*/
Expand All @@ -105,7 +105,7 @@ public function getContents(bool $asString = true)
/**
* Sets the inner contents of the tag (must be pre-escaped if needed)
*
* @param HtmlElement|HtmlElement[]|string $contents
* @param \Stringable|\Stringable[]|string $contents
*
* @return $this
*/
Expand Down
62 changes: 14 additions & 48 deletions tests/functional/Extension/Table/TableMarkdownTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,69 +15,35 @@

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

use League\CommonMark\ConverterInterface;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\Table\TableExtension;
use League\CommonMark\Parser\MarkdownParser;
use League\CommonMark\Renderer\HtmlRenderer;
use League\CommonMark\Renderer\MarkdownRendererInterface;
use PHPUnit\Framework\TestCase;
use League\CommonMark\MarkdownConverter;
use League\CommonMark\Tests\Functional\AbstractLocalDataTest;

/**
* @internal
*/
final class TableMarkdownTest extends TestCase
final class TableMarkdownTest extends AbstractLocalDataTest
{
private Environment $environment;

private MarkdownParser $parser;

protected function setUp(): void
{
$this->environment = new Environment();
$this->environment->addExtension(new CommonMarkCoreExtension());
$this->environment->addExtension(new TableExtension());

$this->parser = new MarkdownParser($this->environment);
}

/**
* @dataProvider dataProvider
* @param array<string, mixed> $config
*/
public function testRenderer(string $markdown, string $html, string $testName): void
protected function createConverter(array $config = []): ConverterInterface
{
$renderer = new HtmlRenderer($this->environment);
$this->assertCommonMark($renderer, $markdown, $html, $testName);
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new TableExtension());

return new MarkdownConverter($environment);
}

/**
* @return array<array<string>>
* {@inheritDoc}
*/
public function dataProvider(): array
public function dataProvider(): iterable
{
$ret = [];
foreach (\glob(__DIR__ . '/md/*.md') as $markdownFile) {
$testName = \basename($markdownFile, '.md');

$markdown = \file_get_contents($markdownFile);
$html = \file_get_contents(__DIR__ . '/md/' . $testName . '.html');

$ret[] = [$markdown, $html, $testName];
}

return $ret;
}

protected function assertCommonMark(MarkdownRendererInterface $renderer, string $markdown, string $html, string $testName): void
{
$documentAST = $this->parser->parse($markdown);
$actualResult = $renderer->renderDocument($documentAST);

$failureMessage = \sprintf('Unexpected result for "%s" test', $testName);
$failureMessage .= "\n=== markdown ===============\n" . $markdown;
$failureMessage .= "\n=== expected ===============\n" . $html;
$failureMessage .= "\n=== got ====================\n" . $actualResult;

$this->assertEquals($html, $actualResult, $failureMessage);
yield from $this->loadTests(__DIR__ . '/md');
}
}
18 changes: 18 additions & 0 deletions tests/functional/Extension/Table/md/wrapped.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<div class="table-responsive"><table>
<thead>
<tr>
<th>header 1</th>
<th>header 2</th>
</tr>
</thead>
<tbody>
<tr>
<td>cell 1.1</td>
<td>cell 1.2</td>
</tr>
<tr>
<td>cell 2.1</td>
<td>cell 2.2</td>
</tr>
</tbody>
</table></div>
11 changes: 11 additions & 0 deletions tests/functional/Extension/Table/md/wrapped.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
table:
wrap:
enabled: true
attributes: { class: "table-responsive" }
---

header 1 | header 2
-------- | --------
cell 1.1 | cell 1.2
cell 2.1 | cell 2.2
39 changes: 39 additions & 0 deletions tests/unit/Renderer/Block/HtmlDecoratorTest.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 <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace League\CommonMark\Tests\Unit\Renderer\Block;

use League\CommonMark\Node\Node;
use League\CommonMark\Renderer\ChildNodeRendererInterface;
use League\CommonMark\Renderer\HtmlDecorator;
use League\CommonMark\Renderer\NodeRendererInterface;
use PHPUnit\Framework\TestCase;

final class HtmlDecoratorTest extends TestCase
{
public function testRender(): void
{
$inner = $this->getMockForAbstractClass(NodeRendererInterface::class);
$inner->method('render')->willReturn('INNER CONTENTS');

$decorator = new HtmlDecorator($inner, 'div', ['class' => 'foo', 'id' => 'bar'], true);

$this->assertSame('<div class="foo" id="bar">INNER CONTENTS</div>', (string) $decorator->render(
$this->getMockForAbstractClass(Node::class),
$this->getMockForAbstractClass(ChildNodeRendererInterface::class)
));
}
}

0 comments on commit 00c1b72

Please sign in to comment.