Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Decorate table output #787

Merged
merged 3 commits into from
Jan 22, 2022
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
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 <[email protected]>
*
* 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 <[email protected]>
*
* 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)
));
}
}