Skip to content

Commit f700eb4

Browse files
authored
Merge pull request #576 from phpDocumentor/feature/table
[FEATURE] Introduce table directive
2 parents 481cc03 + 860aa62 commit f700eb4

File tree

22 files changed

+286
-50
lines changed

22 files changed

+286
-50
lines changed

packages/guides-restructured-text/resources/config/guides-restructured-text.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
use phpDocumentor\Guides\RestructuredText\Directives\SeeAlsoDirective;
4343
use phpDocumentor\Guides\RestructuredText\Directives\SidebarDirective;
4444
use phpDocumentor\Guides\RestructuredText\Directives\SubDirective;
45+
use phpDocumentor\Guides\RestructuredText\Directives\TableDirective;
4546
use phpDocumentor\Guides\RestructuredText\Directives\TipDirective;
4647
use phpDocumentor\Guides\RestructuredText\Directives\TitleDirective;
4748
use phpDocumentor\Guides\RestructuredText\Directives\ToctreeDirective;
@@ -197,6 +198,7 @@
197198
->set(RoleDirective::class)
198199
->set(SeeAlsoDirective::class)
199200
->set(SidebarDirective::class)
201+
->set(TableDirective::class)
200202
->set(TipDirective::class)
201203
->set(TitleDirective::class)
202204
->set(ToctreeDirective::class)

packages/guides-restructured-text/src/RestructuredText/Directives/BaseDirective.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ public function process(
6060
): Node|null {
6161
return $this->processNode($blockContext, $directive)
6262
// Ensure options are always available
63-
->withOptions($this->optionsToArray($directive->getOptions()));
63+
->withKeepExistingOptions($this->optionsToArray($directive->getOptions()));
6464
}
6565

6666
/**

packages/guides-restructured-text/src/RestructuredText/Directives/CsvTableDirective.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@
1717
use Psr\Log\LoggerInterface;
1818

1919
use function array_filter;
20+
use function array_map;
2021
use function count;
22+
use function explode;
2123
use function implode;
24+
use function strval;
2225
use function trim;
2326

2427
/**
@@ -47,6 +50,7 @@ public function processNode(
4750
BlockContext $blockContext,
4851
Directive $directive,
4952
): Node {
53+
$options = $this->optionsToArray($directive->getOptions());
5054
if ($directive->hasOption('file')) {
5155
$csvStream = $blockContext->getDocumentParserContext()
5256
->getContext()
@@ -96,7 +100,17 @@ public function processNode(
96100
$rows[] = $tableRow;
97101
}
98102

99-
return new TableNode($rows, array_filter([$header]));
103+
$tableNode = new TableNode($rows, array_filter([$header]));
104+
if (isset($options['widths']) && $options['widths'] !== 'auto' && $options['widths'] !== 'grid') {
105+
$colWidths = array_map('intval', explode(',', strval($options['widths'])));
106+
// A list of integers is used instead of the input column widths. Implies "grid".
107+
$options['widths'] = 'grid';
108+
$tableNode = $tableNode->withColumnWidth($colWidths);
109+
}
110+
111+
$tableNode = $tableNode->withOptions($options);
112+
113+
return $tableNode;
100114
}
101115

102116
private function buildColumn(

packages/guides-restructured-text/src/RestructuredText/Directives/SubDirective.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ final public function process(
4444
return null;
4545
}
4646

47-
return $node->withOptions($this->optionsToArray($directive->getOptions()));
47+
return $node->withKeepExistingOptions($this->optionsToArray($directive->getOptions()));
4848
}
4949

5050
/** @return Rule<CollectionNode> */
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace phpDocumentor\Guides\RestructuredText\Directives;
6+
7+
use phpDocumentor\Guides\Nodes\Node;
8+
use phpDocumentor\Guides\Nodes\TableNode;
9+
use phpDocumentor\Guides\RestructuredText\Nodes\CollectionNode;
10+
use phpDocumentor\Guides\RestructuredText\Parser\Directive;
11+
use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule;
12+
use Psr\Log\LoggerInterface;
13+
14+
use function array_map;
15+
use function count;
16+
use function explode;
17+
use function sprintf;
18+
use function strval;
19+
20+
/**
21+
* Applies more options to a table
22+
*
23+
* .. table:: Table Title
24+
* :widths: 30,70
25+
* :align: center
26+
* :class: custom-table
27+
*
28+
* +-----------------+-----------------+
29+
* | Header 1 | Header 2 |
30+
* +=================+=================+
31+
* | Row 1, Column 1 | Row 1, Column 2 |
32+
* +-----------------+-----------------+
33+
* | Row 2, Column 1 | Row 2, Column 2 |
34+
* +-----------------+-----------------+
35+
*/
36+
final class TableDirective extends SubDirective
37+
{
38+
public function __construct(
39+
protected Rule $startingRule,
40+
private LoggerInterface $logger,
41+
) {
42+
parent::__construct($startingRule);
43+
}
44+
45+
public function getName(): string
46+
{
47+
return 'table';
48+
}
49+
50+
/** {@inheritDoc} */
51+
protected function processSub(
52+
CollectionNode $collectionNode,
53+
Directive $directive,
54+
): Node|null {
55+
if (count($collectionNode->getChildren()) !== 1) {
56+
$this->logger->warning(sprintf('The table directive may contain exactly one table. %s children found', count($collectionNode->getChildren())));
57+
58+
return $collectionNode;
59+
}
60+
61+
$tableNode = $collectionNode->getChildren()[0];
62+
if (!$tableNode instanceof TableNode) {
63+
$this->logger->warning(sprintf('The table directive may contain exactly one table. A node of type %s was found. ', $tableNode::class));
64+
65+
return $collectionNode;
66+
}
67+
68+
$options = $this->optionsToArray($directive->getOptions());
69+
if (isset($options['widths']) && $options['widths'] !== 'auto' && $options['widths'] !== 'grid') {
70+
$colWidths = array_map('intval', explode(',', strval($options['widths'])));
71+
// A list of integers is used instead of the input column widths. Implies "grid".
72+
$options['widths'] = 'grid';
73+
$tableNode = $tableNode->withColumnWidth($colWidths);
74+
}
75+
76+
return $tableNode->withOptions($options);
77+
}
78+
}

packages/guides-restructured-text/tests/unit/Parser/DummyNode.php

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ public function withOptions(array $options): Node
3636
return $this;
3737
}
3838

39+
/** {@inheritDoc} */
40+
public function withKeepExistingOptions(array $options): Node
41+
{
42+
return $this;
43+
}
44+
3945
public function hasOption(string $name): bool
4046
{
4147
return false;
Lines changed: 6 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,7 @@
1-
<table{% if tableNode.classes %} class="{{ tableNode.classesString }}"{% endif %}>
2-
{% if tableHeaderRows is not empty %}
3-
<thead>
4-
{% for tableHeaderRow in tableHeaderRows %}
5-
<tr>
6-
{% for column in tableHeaderRow.columns %}
7-
<th{% if column.colspan > 1 %} colspan="{{ column.colspan }}"{% endif %}>
8-
{%- for child in column.children -%}
9-
{{- renderNode(child) -}}
10-
{% endfor %}</th>
11-
{% endfor %}
12-
</tr>
13-
{% endfor %}
14-
</thead>
15-
{% endif %}
16-
17-
<tbody>
18-
{% for tableRow in tableRows %}
19-
<tr>
20-
{% for column in tableRow.columns %}
21-
<td{% if column.colSpan > 1 %} colspan="{{ column.colSpan }}"{% endif %}{% if column.rowSpan > 1 %} rowspan="{{ column.rowSpan }}"{% endif %}>
22-
{%- for child in column.children -%}
23-
{{- renderNode(child) -}}
24-
{%- else %}&nbsp;{% endfor %}</td>
25-
{% endfor %}
26-
</tr>
27-
{% endfor %}
28-
</tbody>
1+
<table {%- include "body/table/table-classes.html.twig" -%}
2+
{%- include "body/table/table-inline-style.html.twig" -%}>
3+
{% include "body/table/table-caption.html.twig" %}
4+
{% include "body/table/table-colgroups.html.twig" %}
5+
{% include "body/table/table-header.html.twig" %}
6+
{% include "body/table/table-body.html.twig" %}
297
</table>
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<tbody>
2+
{% for tableRow in tableRows %}
3+
{% include "body/table/table-row.html.twig" %}
4+
{% endfor %}
5+
</tbody>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{% if tableNode.hasOption('caption') %}
2+
<caption>{{ tableNode.option('caption') }}</caption>
3+
{% endif %}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<td{% if column.colSpan > 1 %} colspan="{{ column.colSpan }}"{% endif %}{% if column.rowSpan > 1 %} rowspan="{{ column.rowSpan }}"{% endif %}>
2+
{%- for child in column.children -%}
3+
{{- renderNode(child) -}}
4+
{%- else %}&nbsp;{% endfor %}</td>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{%- set tableClasses = '' -%}
2+
3+
{# Check and add classes conditionally #}
4+
{%- if tableNode.classes -%}
5+
{%- set tableClasses = tableClasses ~ tableNode.classesString ~ ' ' -%}
6+
{%- endif -%}
7+
{%- if tableNode.hasOption('align') -%}
8+
{%- set tableClasses = tableClasses ~ 'align-' ~ tableNode.option('align') ~ ' ' -%}
9+
{%- endif -%}
10+
{%- if tableNode.hasOption('widths') -%}
11+
{%- set tableClasses = tableClasses ~ 'colwidths-' ~ tableNode.option('widths') ~ ' ' -%}
12+
{%- endif -%}
13+
{%- if tableNode.hasOption('grid') == 'grid' -%}
14+
{%- set tableClasses = tableClasses ~ 'grid-' ~ tableNode.option('grid') ~ ' ' -%}
15+
{%- endif -%}
16+
17+
{%- if tableClasses|trim %} class="{{ tableClasses|trim }}"{% endif -%}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{% if tableNode.columnWidth %}
2+
<colgroup>
3+
{% for width in tableNode.columnWidth %}
4+
{% if width<=0 %}
5+
<col style="width: auto">
6+
{% else %}
7+
<col style="width: {{ width }}%">
8+
{% endif %}
9+
{% endfor %}
10+
</colgroup>
11+
{% endif %}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{% if tableHeaderRows is not empty %}
2+
<thead>
3+
{% for tableHeaderRow in tableHeaderRows %}
4+
<tr>
5+
{% for column in tableHeaderRow.columns %}
6+
<th{% if column.colspan > 1 %} colspan="{{ column.colspan }}"{% endif %}>
7+
{%- for child in column.children -%}
8+
{{- renderNode(child) -}}
9+
{% endfor %}</th>
10+
{% endfor %}
11+
</tr>
12+
{% endfor %}
13+
</thead>
14+
{% endif %}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{%- set tableStyle = '' -%}
2+
{%- if tableNode.hasOption('width') -%}
3+
{%- set tableStyle = tableStyle ~ 'width: ' ~ tableNode.option('width') ~ '; ' -%}
4+
{%- endif -%}
5+
{%- if tableStyle|trim %} style="{{ tableStyle|trim }}"{% endif -%}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<tr>
2+
{% for column in tableRow.columns %}
3+
{% include "body/table/table-cell.html.twig" %}
4+
{% endfor %}
5+
</tr>

packages/guides/src/Nodes/AbstractNode.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,21 @@ public function withOptions(array $options): Node
8989
return $result;
9090
}
9191

92+
/**
93+
* Adds $options as default options without overriding any options already set.
94+
*
95+
* @param array<string, scalar|null> $options
96+
*
97+
* @return static
98+
*/
99+
public function withKeepExistingOptions(array $options): Node
100+
{
101+
$result = clone $this;
102+
$result->options = [...$options, ...$result->options];
103+
104+
return $result;
105+
}
106+
92107
public function hasOption(string $name): bool
93108
{
94109
return isset($this->options[$name]);

packages/guides/src/Nodes/Node.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ public function getOptions(): array;
2121
/** @param array<string, scalar|null> $options */
2222
public function withOptions(array $options): Node;
2323

24+
/** @param array<string, scalar|null> $options */
25+
public function withKeepExistingOptions(array $options): Node;
26+
2427
public function hasOption(string $name): bool;
2528

2629
public function setValue(mixed $value): void;

packages/guides/src/Nodes/TableNode.php

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@ class TableNode extends CompoundNode
2424
/**
2525
* @param TableRow[] $data
2626
* @param TableRow[] $headers
27+
* @param int[] $columnWidth
2728
*/
28-
public function __construct(protected array $data, protected array $headers = [])
29+
public function __construct(protected array $data, protected array $headers = [], protected array $columnWidth = [])
2930
{
3031
parent::__construct();
3132
}
@@ -56,4 +57,19 @@ public function getHeaders(): array
5657
{
5758
return $this->headers;
5859
}
60+
61+
/** @return int[] */
62+
public function getColumnWidth(): array
63+
{
64+
return $this->columnWidth;
65+
}
66+
67+
/** @param int[] $columnWidth */
68+
public function withColumnWidth(array $columnWidth): TableNode
69+
{
70+
$table = clone $this;
71+
$table->columnWidth = $columnWidth;
72+
73+
return $table;
74+
}
5975
}

tests/Integration/tests/table-csv-table-with-content/expected/index.html

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -8,24 +8,27 @@
88
<div class="section" id="csv-table-with-content">
99
<h1>Csv table with content</h1>
1010

11-
<table>
12-
<thead>
13-
<tr>
14-
<th>Header 1</th>
15-
<th>Header 2</th>
16-
</tr>
17-
</thead>
18-
11+
<table class="colwidths-grid">
12+
<colgroup>
13+
<col style="width: 30%">
14+
<col style="width: 70%">
15+
</colgroup>
16+
<thead>
17+
<tr>
18+
<th>Header 1</th>
19+
<th>Header 2</th>
20+
</tr>
21+
</thead>
1922
<tbody>
20-
<tr>
21-
<td>1</td>
22-
<td>one</td>
23-
</tr>
24-
<tr>
25-
<td>2</td>
26-
<td>two</td>
27-
</tr>
28-
</tbody>
23+
<tr>
24+
<td>1</td>
25+
<td>one</td>
26+
</tr>
27+
<tr>
28+
<td>2</td>
29+
<td>two</td>
30+
</tr>
31+
</tbody>
2932
</table>
3033

3134
</div>

tests/Integration/tests/table-csv-table-with-content/input/index.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Csv table with content
44

55
.. csv-table:: Numbers
66
:header: "Header 1", "Header 2"
7-
:widths: 15, 15
7+
:widths: 30, 70
88

99
1, "one"
1010
2, "two"

0 commit comments

Comments
 (0)