Skip to content
Draft
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
115 changes: 115 additions & 0 deletions docs/enhancements.md
Original file line number Diff line number Diff line change
Expand Up @@ -706,6 +706,120 @@ Renders the second cell as: `<code>this is a really long code span</code>`

---

### Creole-style Header Cells (`|=`)

**Related:** [php-collective/djot-php#8](https://github.com/php-collective/djot-php/pull/8)

**Status:** Implemented in djot-php

Mark individual cells as headers using the `|=` prefix (Creole/MediaWiki-style):

```djot
|= Name |= Age |
| Alice | 28 |
| Bob | 34 |
```

**Output:**
```html
<table>
<tr><th>Name</th><th>Age</th></tr>
<tr><td>Alice</td><td>28</td></tr>
<tr><td>Bob</td><td>34</td></tr>
</table>
```

**Key Differences from Separator Row Headers:**
- No separator row (`|---|`) needed
- Individual cells can be headers (mix header and data cells in same row)
- Enables row headers on the left side of tables

**Row Headers Example:**

```djot
|= Category | Value |
|= Apples | 10 |
|= Oranges | 20 |
```

Each row has a header cell on the left and a data cell on the right.

**Inline Alignment:**

Alignment markers attach directly to `|=`:

| Syntax | Alignment |
|--------|-----------|
| `\|=< text` | Left |
| `\|=> text` | Right |
| `\|=~ text` | Center |

```djot
|=< Left |=> Right |=~ Center |
| A | B | C |
```

Header alignment propagates to data cells below when no separator row is present.

**Combining with Attributes:**

Attributes come after the `=` marker (and alignment if present):

```djot
|={.name} Name |={.age} Age |
| Alice | 28 |
```

Output: `<th class="name">Name</th><th class="age">Age</th>`

Order: `|=` `[alignment]` `[{attributes}]` `content`

```djot
|=<{.left} Left |=>{#right .highlight} Right |
| A | B |
```

Output:
- `<th class="left" style="text-align: left;">Left</th>`
- `<th id="right" class="highlight" style="text-align: right;">Right</th>`

**Combining with Colspan/Rowspan:**

Headers work with `<` (colspan) and `^` (rowspan) markers:

```djot
|=~ Report Title | < |
|= Category |= Items |
| Fruits | Apple |
| ^ | Banana |
```

Output:
- "Report Title" → `<th colspan="2" style="text-align: center;">`
- "Category" → `<th rowspan="2">` (extended by `^` markers below)

**Combining with Multi-line Cells:**

Continuation rows (`+`) merge content into header cells:

```djot
|= Long Header Name |= Short |
+ (continued) | |
| data | data |
```

Output: `<th>Long Header Name (continued)</th>`

Note: `=` in continuation rows is treated as content, not a header marker.

**Compatibility Notes:**

- Markers must be directly attached to pipe: `|= Header` (header), `| = text` (literal)
- Can coexist with separator rows (separator row alignment takes precedence)
- Works with colspan (`<`), rowspan (`^`), and multi-line cells (`+`)

---

### Captions for Images, Tables, and Block Quotes

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

### Optional Modes
Expand Down
78 changes: 78 additions & 0 deletions src/Parser/Block/TableParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -465,6 +465,84 @@ public function isColspanMarker(string $cellContent): bool
return trim($cellContent) === '<';
}

/**
* Check if cell content starts with = (Creole-style header marker).
* The = must be directly attached to the pipe: |= Header | is a header,
* but | = text | is literal content "= text".
*
* @param string $cellContent The raw cell content (not trimmed)
*
* @return bool True if the cell is a header cell
*/
public function isHeaderMarker(string $cellContent): bool
{
return str_starts_with($cellContent, '=');
}

/**
* Parse header cell content and extract alignment and attributes.
* Supports: |= Header |, |=< Left |, |=> Right |, |=~ Center |, |={.class} Header |
*
* Order: |= [alignment] [{attributes}] content |
* Examples: |=< Header |, |={.class} Header |, |=>{#id .class} Header |
*
* @param string $cellContent The raw cell content starting with =
*
* @return array{content: string, alignment: string, attributes: array<string, string>} Parsed data
*/
public function parseHeaderCell(string $cellContent): array
{
// Remove the leading =
$afterEquals = substr($cellContent, 1);
$alignment = TableCell::ALIGN_DEFAULT;
$attributes = [];

// Check for alignment marker (must be directly attached: =< not = <)
if (str_starts_with($afterEquals, '<')) {
$alignment = TableCell::ALIGN_LEFT;
$afterEquals = substr($afterEquals, 1);
} elseif (str_starts_with($afterEquals, '>')) {
$alignment = TableCell::ALIGN_RIGHT;
$afterEquals = substr($afterEquals, 1);
} elseif (str_starts_with($afterEquals, '~')) {
$alignment = TableCell::ALIGN_CENTER;
$afterEquals = substr($afterEquals, 1);
}

// Check for attributes after alignment marker: |={.class} or |=<{.class}
if (str_starts_with($afterEquals, '{')) {
// Find matching closing brace
$braceDepth = 0;
$endPos = -1;
$len = strlen($afterEquals);

for ($i = 0; $i < $len; $i++) {
if ($afterEquals[$i] === '{') {
$braceDepth++;
} elseif ($afterEquals[$i] === '}') {
$braceDepth--;
if ($braceDepth === 0) {
$endPos = $i;

break;
}
}
}

if ($endPos > 0) {
$attrStr = substr($afterEquals, 1, $endPos - 1);
$attributes = AttributeParser::parse($attrStr);
$afterEquals = substr($afterEquals, $endPos + 1);
}
}

return [
'content' => trim($afterEquals),
'alignment' => $alignment,
'attributes' => $attributes,
];
}

/**
* Check if a line is a continuation row (starts with +).
* Continuation rows use + prefix instead of | to signal that the contents
Expand Down
58 changes: 46 additions & 12 deletions src/Parser/BlockParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -2223,36 +2223,57 @@ protected function tryParseTable(Node $parent, array $lines, int $start): ?int
}
}

// Parse regular row
$row = new TableRow(false);
if ($rowAttributes) {
$row->setAttributes($rowAttributes);
}

// Store row data for rowspan processing
// Track column positions for cells accounting for rowspan markers
$rowCellData = [];
$colPosition = 0;
$rowHasHeaderCell = false;

foreach ($processedCells as $index => $cellData) {
$colspan = $cellData['colspan'];
$cellContent = $cellData['content'];

// Check for rowspan marker
if ($this->tableParser->isRowspanMarker($cellData['content'])) {
if ($this->tableParser->isRowspanMarker($cellContent)) {
// Mark this position for rowspan processing
$rowCellData[] = [
'type' => 'rowspan_marker',
'colPosition' => $colPosition,
];
$colPosition += $colspan;
} else {
$isHeader = false;
$alignment = $alignments[$index] ?? TableCell::ALIGN_DEFAULT;
$cell = new TableCell(false, $alignment, 1, $colspan);
if ($cellData['attributes']) {
$cell->setAttributes($cellData['attributes']);
$contentToParse = trim($cellContent);

// Check for |= header marker (Creole-style)
$headerAttributes = [];
if ($this->tableParser->isHeaderMarker($cellContent)) {
$isHeader = true;
$rowHasHeaderCell = true;
$headerData = $this->tableParser->parseHeaderCell($cellContent);
$contentToParse = $headerData['content'];
$headerAttributes = $headerData['attributes'];

// Header alignment takes precedence if no separator row alignment
if ($headerData['alignment'] !== TableCell::ALIGN_DEFAULT) {
$alignment = $headerData['alignment'];
// Store alignment for propagation to subsequent data cells
if (!isset($alignments[$index])) {
$alignments[$index] = $alignment;
}
}
}

$cell = new TableCell($isHeader, $alignment, 1, $colspan);

// Merge cell attributes: header attributes (|={.class}) take precedence,
// then cell attributes (|{.class}=), allowing both syntaxes
$mergedAttributes = array_merge($cellData['attributes'], $headerAttributes);
if ($mergedAttributes) {
$cell->setAttributes($mergedAttributes);
}
$this->inlineParser->parse($cell, trim($cellData['content']), $baseLineForRow);
$row->appendChild($cell);
$this->inlineParser->parse($cell, $contentToParse, $baseLineForRow);
$rowCellData[] = [
'type' => 'cell',
'cell' => $cell,
Expand All @@ -2262,6 +2283,19 @@ protected function tryParseTable(Node $parent, array $lines, int $start): ?int
}
}

// Create the row (mark as header row if any cell has |= syntax)
$row = new TableRow($rowHasHeaderCell);
if ($rowAttributes) {
$row->setAttributes($rowAttributes);
}

// Append cells to the row
foreach ($rowCellData as $cellInfo) {
if ($cellInfo['type'] === 'cell' && isset($cellInfo['cell'])) {
$row->appendChild($cellInfo['cell']);
}
}

// Process rowspan markers - find cells above that should span down
// We need to track column positions considering rowspan markers in previous rows
$tableChildren = $table->getChildren();
Expand Down
18 changes: 13 additions & 5 deletions src/Renderer/HtmlRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,19 @@ protected function renderTableRow(TableRow $node): string
protected function renderTableCell(TableCell $node): string
{
$tag = $node->isHeader() ? 'th' : 'td';

// Handle alignment - merge with existing style attribute if present
$alignment = $node->getAlignment();
if ($alignment !== TableCell::ALIGN_DEFAULT) {
$existingStyle = $node->getAttribute('style');
if ($existingStyle !== null) {
// Merge: prepend alignment to existing style
$node->setAttribute('style', 'text-align: ' . $alignment . '; ' . $existingStyle);
} else {
$node->setAttribute('style', 'text-align: ' . $alignment . ';');
}
}

$attrs = $this->renderAttributes($node);

$rowspan = $node->getRowspan();
Expand All @@ -721,11 +734,6 @@ protected function renderTableCell(TableCell $node): string
$attrs .= ' colspan="' . $colspan . '"';
}

$alignment = $node->getAlignment();
if ($alignment !== TableCell::ALIGN_DEFAULT) {
$attrs .= ' style="text-align: ' . $alignment . ';"';
}

return '<' . $tag . $attrs . '>' . $this->renderChildren($node) . '</' . $tag . ">\n";
}

Expand Down
Loading