Skip to content

Commit 4967fca

Browse files
committed
Add Creole-style |= table header syntax
Adds support for marking individual cells as headers using the |= prefix (Creole/MediaWiki-style syntax). This enables: - Headers without requiring a separator row - Mixed header and data cells in the same row - Row headers on the left side of tables - Inline alignment markers: |=< (left), |=> (right), |=~ (center) The marker must be directly attached to the pipe character to be recognized as a header (|= is header, | = is literal content). This feature integrates cleanly with the existing table spanning features (colspan with <, rowspan with ^, and multi-line cells with +).
1 parent 0b8e068 commit 4967fca

4 files changed

Lines changed: 424 additions & 10 deletions

File tree

docs/enhancements.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -706,6 +706,69 @@ Renders the second cell as: `<code>this is a really long code span</code>`
706706

707707
---
708708

709+
### Creole-style Header Cells (`|=`)
710+
711+
**Related:** [php-collective/djot-php#8](https://github.com/php-collective/djot-php/pull/8)
712+
713+
**Status:** Implemented in djot-php
714+
715+
Mark individual cells as headers using the `|=` prefix (Creole/MediaWiki-style):
716+
717+
```djot
718+
|= Name |= Age |
719+
| Alice | 28 |
720+
| Bob | 34 |
721+
```
722+
723+
**Output:**
724+
```html
725+
<table>
726+
<tr><th>Name</th><th>Age</th></tr>
727+
<tr><td>Alice</td><td>28</td></tr>
728+
<tr><td>Bob</td><td>34</td></tr>
729+
</table>
730+
```
731+
732+
**Key Differences from Separator Row Headers:**
733+
- No separator row (`|---|`) needed
734+
- Individual cells can be headers (mix header and data cells in same row)
735+
- Enables row headers on the left side of tables
736+
737+
**Row Headers Example:**
738+
739+
```djot
740+
|= Category | Value |
741+
|= Apples | 10 |
742+
|= Oranges | 20 |
743+
```
744+
745+
Each row has a header cell on the left and a data cell on the right.
746+
747+
**Inline Alignment:**
748+
749+
Alignment markers attach directly to `|=`:
750+
751+
| Syntax | Alignment |
752+
|--------|-----------|
753+
| `\|=< text` | Left |
754+
| `\|=> text` | Right |
755+
| `\|=~ text` | Center |
756+
757+
```djot
758+
|=< Left |=> Right |=~ Center |
759+
| A | B | C |
760+
```
761+
762+
Header alignment propagates to data cells below when no separator row is present.
763+
764+
**Compatibility:**
765+
766+
- Markers must be directly attached to pipe: `|= Header` (header), `| = text` (literal)
767+
- Can coexist with separator rows (separator row alignment takes precedence)
768+
- Works with colspan (`<`), rowspan (`^`), and multi-line cells (`+`)
769+
770+
---
771+
709772
### Captions for Images, Tables, and Block Quotes
710773

711774
**Related:** [php-collective/djot-php#37](https://github.com/php-collective/djot-php/issues/37)
@@ -820,6 +883,7 @@ vendor/bin/phpunit
820883
| Fenced comment blocks | [djot:67](https://github.com/jgm/djot/issues/67) | Open |
821884
| Captions (image/table/blockquote) | [#37](https://github.com/php-collective/djot-php/issues/37) | djot-php |
822885
| Table multi-line/rowspan/colspan | [djot:368](https://github.com/jgm/djot/issues/368) | Open |
886+
| Creole-style header cells (`\|=`) | [#8](https://github.com/php-collective/djot-php/pull/8) | djot-php |
823887
| Abbreviations (block, not inline) | [djot:51](https://github.com/jgm/djot/issues/51) | djot-php |
824888

825889
### Optional Modes

src/Parser/Block/TableParser.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -465,6 +465,52 @@ public function isColspanMarker(string $cellContent): bool
465465
return trim($cellContent) === '<';
466466
}
467467

468+
/**
469+
* Check if cell content starts with = (Creole-style header marker).
470+
* The = must be directly attached to the pipe: |= Header | is a header,
471+
* but | = text | is literal content "= text".
472+
*
473+
* @param string $cellContent The raw cell content (not trimmed)
474+
*
475+
* @return bool True if the cell is a header cell
476+
*/
477+
public function isHeaderMarker(string $cellContent): bool
478+
{
479+
return str_starts_with($cellContent, '=');
480+
}
481+
482+
/**
483+
* Parse header cell content and extract alignment.
484+
* Supports: |= Header |, |=< Left |, |=> Right |, |=~ Center |
485+
*
486+
* @param string $cellContent The raw cell content starting with =
487+
*
488+
* @return array{content: string, alignment: string} Parsed content and alignment
489+
*/
490+
public function parseHeaderCell(string $cellContent): array
491+
{
492+
// Remove the leading =
493+
$afterEquals = substr($cellContent, 1);
494+
$alignment = TableCell::ALIGN_DEFAULT;
495+
496+
// Check for alignment marker (must be directly attached: =< not = <)
497+
if (str_starts_with($afterEquals, '<')) {
498+
$alignment = TableCell::ALIGN_LEFT;
499+
$afterEquals = substr($afterEquals, 1);
500+
} elseif (str_starts_with($afterEquals, '>')) {
501+
$alignment = TableCell::ALIGN_RIGHT;
502+
$afterEquals = substr($afterEquals, 1);
503+
} elseif (str_starts_with($afterEquals, '~')) {
504+
$alignment = TableCell::ALIGN_CENTER;
505+
$afterEquals = substr($afterEquals, 1);
506+
}
507+
508+
return [
509+
'content' => trim($afterEquals),
510+
'alignment' => $alignment,
511+
];
512+
}
513+
468514
/**
469515
* Check if a line is a continuation row (starts with +).
470516
* Continuation rows use + prefix instead of | to signal that the contents

src/Parser/BlockParser.php

Lines changed: 38 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2223,36 +2223,51 @@ protected function tryParseTable(Node $parent, array $lines, int $start): ?int
22232223
}
22242224
}
22252225

2226-
// Parse regular row
2227-
$row = new TableRow(false);
2228-
if ($rowAttributes) {
2229-
$row->setAttributes($rowAttributes);
2230-
}
2231-
22322226
// Store row data for rowspan processing
22332227
// Track column positions for cells accounting for rowspan markers
22342228
$rowCellData = [];
22352229
$colPosition = 0;
2230+
$rowHasHeaderCell = false;
22362231

22372232
foreach ($processedCells as $index => $cellData) {
22382233
$colspan = $cellData['colspan'];
2234+
$cellContent = $cellData['content'];
22392235

22402236
// Check for rowspan marker
2241-
if ($this->tableParser->isRowspanMarker($cellData['content'])) {
2237+
if ($this->tableParser->isRowspanMarker($cellContent)) {
22422238
// Mark this position for rowspan processing
22432239
$rowCellData[] = [
22442240
'type' => 'rowspan_marker',
22452241
'colPosition' => $colPosition,
22462242
];
22472243
$colPosition += $colspan;
22482244
} else {
2245+
$isHeader = false;
22492246
$alignment = $alignments[$index] ?? TableCell::ALIGN_DEFAULT;
2250-
$cell = new TableCell(false, $alignment, 1, $colspan);
2247+
$contentToParse = trim($cellContent);
2248+
2249+
// Check for |= header marker (Creole-style)
2250+
if ($this->tableParser->isHeaderMarker($cellContent)) {
2251+
$isHeader = true;
2252+
$rowHasHeaderCell = true;
2253+
$headerData = $this->tableParser->parseHeaderCell($cellContent);
2254+
$contentToParse = $headerData['content'];
2255+
2256+
// Header alignment takes precedence if no separator row alignment
2257+
if ($headerData['alignment'] !== TableCell::ALIGN_DEFAULT) {
2258+
$alignment = $headerData['alignment'];
2259+
// Store alignment for propagation to subsequent data cells
2260+
if (!isset($alignments[$index])) {
2261+
$alignments[$index] = $alignment;
2262+
}
2263+
}
2264+
}
2265+
2266+
$cell = new TableCell($isHeader, $alignment, 1, $colspan);
22512267
if ($cellData['attributes']) {
22522268
$cell->setAttributes($cellData['attributes']);
22532269
}
2254-
$this->inlineParser->parse($cell, trim($cellData['content']), $baseLineForRow);
2255-
$row->appendChild($cell);
2270+
$this->inlineParser->parse($cell, $contentToParse, $baseLineForRow);
22562271
$rowCellData[] = [
22572272
'type' => 'cell',
22582273
'cell' => $cell,
@@ -2262,6 +2277,19 @@ protected function tryParseTable(Node $parent, array $lines, int $start): ?int
22622277
}
22632278
}
22642279

2280+
// Create the row (mark as header row if any cell has |= syntax)
2281+
$row = new TableRow($rowHasHeaderCell);
2282+
if ($rowAttributes) {
2283+
$row->setAttributes($rowAttributes);
2284+
}
2285+
2286+
// Append cells to the row
2287+
foreach ($rowCellData as $cellInfo) {
2288+
if ($cellInfo['type'] === 'cell' && isset($cellInfo['cell'])) {
2289+
$row->appendChild($cellInfo['cell']);
2290+
}
2291+
}
2292+
22652293
// Process rowspan markers - find cells above that should span down
22662294
// We need to track column positions considering rowspan markers in previous rows
22672295
$tableChildren = $table->getChildren();

0 commit comments

Comments
 (0)