From 09c86ed0505b12ee1622e42da402fb6f79745130 Mon Sep 17 00:00:00 2001 From: mscherer Date: Sun, 30 Nov 2025 03:17:49 +0100 Subject: [PATCH 1/2] Add Creole-style |= table header syntax Adds support for marking header cells with |= prefix instead of requiring a separator row. This is simpler and more intuitive, especially for users coming from wiki markup. Features: - |= Cell marks a header cell - |=< Left-aligned header - |=> Right-aligned header - |=~ Center-aligned header The traditional separator row syntax continues to work unchanged. See: https://github.com/jgm/djot/issues/354 --- src/Parser/BlockParser.php | 44 +++++++++++-- tests/TestCase/Parser/BlockParserTest.php | 80 +++++++++++++++++++++++ 2 files changed, 120 insertions(+), 4 deletions(-) diff --git a/src/Parser/BlockParser.php b/src/Parser/BlockParser.php index a8c705b..117d9d3 100644 --- a/src/Parser/BlockParser.php +++ b/src/Parser/BlockParser.php @@ -2229,13 +2229,49 @@ protected function tryParseTable(Node $parent, array $lines, int $start): ?int } // Parse regular row - $row = new TableRow(false); $cells = $this->parseTableCells($currentLine); + $rowHasHeaderCell = false; + $parsedCells = []; foreach ($cells as $index => $cellContent) { - $alignment = $alignments[$index] ?? TableCell::ALIGN_DEFAULT; - $cell = new TableCell(false, $alignment); - $this->inlineParser->parse($cell, trim($cellContent), $i); + $trimmed = trim($cellContent); + $isHeader = false; + $cellAlignment = $alignments[$index] ?? TableCell::ALIGN_DEFAULT; + + // Check for |= header cell syntax (Creole-style) + // Supports: |= Header |=< Left |=> Right |=~ Center + if (str_starts_with($trimmed, '=')) { + $isHeader = true; + $rowHasHeaderCell = true; + $trimmed = substr($trimmed, 1); // Remove = + + // Check for alignment marker after = + if (str_starts_with($trimmed, '<')) { + $cellAlignment = TableCell::ALIGN_LEFT; + $trimmed = substr($trimmed, 1); + } elseif (str_starts_with($trimmed, '>')) { + $cellAlignment = TableCell::ALIGN_RIGHT; + $trimmed = substr($trimmed, 1); + } elseif (str_starts_with($trimmed, '~')) { + $cellAlignment = TableCell::ALIGN_CENTER; + $trimmed = substr($trimmed, 1); + } + + $cellContent = $trimmed; + } + + $parsedCells[] = [ + 'content' => trim($cellContent), + 'isHeader' => $isHeader, + 'alignment' => $cellAlignment, + ]; + } + + // Create the row (header row if any cell has |= syntax) + $row = new TableRow($rowHasHeaderCell); + foreach ($parsedCells as $cellData) { + $cell = new TableCell($cellData['isHeader'], $cellData['alignment']); + $this->inlineParser->parse($cell, $cellData['content'], $i); $row->appendChild($cell); } diff --git a/tests/TestCase/Parser/BlockParserTest.php b/tests/TestCase/Parser/BlockParserTest.php index 20a1e38..f5dc494 100644 --- a/tests/TestCase/Parser/BlockParserTest.php +++ b/tests/TestCase/Parser/BlockParserTest.php @@ -12,6 +12,8 @@ use Djot\Node\Block\ListBlock; use Djot\Node\Block\Paragraph; use Djot\Node\Block\Table; +use Djot\Node\Block\TableCell; +use Djot\Node\Block\TableRow; use Djot\Node\Block\ThematicBreak; use Djot\Node\Document; use Djot\Parser\BlockParser; @@ -204,6 +206,84 @@ public function testParseTable(): void $this->assertInstanceOf(Table::class, $doc->getChildren()[0]); } + public function testParseTableWithEqualsHeaderSyntax(): void + { + // Creole-style |= header syntax (no separator row needed) + $doc = $this->parser->parse("|= Name |= Age |\n| Alice | 28 |"); + + $this->assertCount(1, $doc->getChildren()); + $table = $doc->getChildren()[0]; + $this->assertInstanceOf(Table::class, $table); + + $rows = $table->getChildren(); + $this->assertCount(2, $rows); + + // First row should be a header row + $headerRow = $rows[0]; + $this->assertInstanceOf(TableRow::class, $headerRow); + $this->assertTrue($headerRow->isHeader()); + + // Header cells should be marked as headers + $headerCells = $headerRow->getChildren(); + $this->assertCount(2, $headerCells); + $this->assertInstanceOf(TableCell::class, $headerCells[0]); + $this->assertTrue($headerCells[0]->isHeader()); + $this->assertTrue($headerCells[1]->isHeader()); + + // Second row should be a data row + $dataRow = $rows[1]; + $this->assertInstanceOf(TableRow::class, $dataRow); + $this->assertFalse($dataRow->isHeader()); + } + + public function testParseTableWithEqualsHeaderAlignment(): void + { + // |=< left, |=> right, |=~ center + $doc = $this->parser->parse("|=< Left |=> Right |=~ Center |\n| A | B | C |"); + + $table = $doc->getChildren()[0]; + $this->assertInstanceOf(Table::class, $table); + + $headerRow = $table->getChildren()[0]; + $cells = $headerRow->getChildren(); + + $this->assertSame(TableCell::ALIGN_LEFT, $cells[0]->getAlignment()); + $this->assertSame(TableCell::ALIGN_RIGHT, $cells[1]->getAlignment()); + $this->assertSame(TableCell::ALIGN_CENTER, $cells[2]->getAlignment()); + } + + public function testParseTableWithMixedHeaderCells(): void + { + // Mix of header and regular cells in a row + $doc = $this->parser->parse("|= Header | Regular |\n| Data | Data |"); + + $table = $doc->getChildren()[0]; + $rows = $table->getChildren(); + + // Row with any header cell is marked as header row + $firstRow = $rows[0]; + $this->assertTrue($firstRow->isHeader()); + + $cells = $firstRow->getChildren(); + $this->assertTrue($cells[0]->isHeader()); + $this->assertFalse($cells[1]->isHeader()); + } + + public function testParseTableWithEqualsHeaderNoSeparatorNeeded(): void + { + // Unlike traditional tables, |= syntax doesn't need separator row + $doc = $this->parser->parse("|= A |= B |\n| 1 | 2 |\n| 3 | 4 |"); + + $table = $doc->getChildren()[0]; + $rows = $table->getChildren(); + + // Should have 3 rows (1 header + 2 data), no separator consumed + $this->assertCount(3, $rows); + $this->assertTrue($rows[0]->isHeader()); + $this->assertFalse($rows[1]->isHeader()); + $this->assertFalse($rows[2]->isHeader()); + } + public function testParseBlockAttributes(): void { $doc = $this->parser->parse("{.highlight}\n# Heading"); From ea5d384b5b790ccbf40bbf298da1fdb66fc8541e Mon Sep 17 00:00:00 2001 From: mscherer Date: Wed, 3 Dec 2025 01:09:48 +0100 Subject: [PATCH 2/2] Propagate header cell alignment to column when no separator row When using |=< |=> |=~ alignment markers on header cells, the alignment now applies to the entire column (subsequent data rows), not just the header cell itself. Separator row alignment still takes precedence if present. --- src/Parser/BlockParser.php | 10 +++++ tests/TestCase/Parser/BlockParserTest.php | 45 +++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/Parser/BlockParser.php b/src/Parser/BlockParser.php index 117d9d3..4628f5a 100644 --- a/src/Parser/BlockParser.php +++ b/src/Parser/BlockParser.php @@ -2246,15 +2246,25 @@ protected function tryParseTable(Node $parent, array $lines, int $start): ?int $trimmed = substr($trimmed, 1); // Remove = // Check for alignment marker after = + // This sets column alignment if no separator row defined it if (str_starts_with($trimmed, '<')) { $cellAlignment = TableCell::ALIGN_LEFT; $trimmed = substr($trimmed, 1); + if (!isset($alignments[$index])) { + $alignments[$index] = TableCell::ALIGN_LEFT; + } } elseif (str_starts_with($trimmed, '>')) { $cellAlignment = TableCell::ALIGN_RIGHT; $trimmed = substr($trimmed, 1); + if (!isset($alignments[$index])) { + $alignments[$index] = TableCell::ALIGN_RIGHT; + } } elseif (str_starts_with($trimmed, '~')) { $cellAlignment = TableCell::ALIGN_CENTER; $trimmed = substr($trimmed, 1); + if (!isset($alignments[$index])) { + $alignments[$index] = TableCell::ALIGN_CENTER; + } } $cellContent = $trimmed; diff --git a/tests/TestCase/Parser/BlockParserTest.php b/tests/TestCase/Parser/BlockParserTest.php index f5dc494..94a1c8c 100644 --- a/tests/TestCase/Parser/BlockParserTest.php +++ b/tests/TestCase/Parser/BlockParserTest.php @@ -284,6 +284,51 @@ public function testParseTableWithEqualsHeaderNoSeparatorNeeded(): void $this->assertFalse($rows[2]->isHeader()); } + public function testParseTableWithEqualsHeaderAlignmentPropagates(): void + { + // Header alignment should propagate to data cells when no separator row + $doc = $this->parser->parse("|=> Right |=< Left |=~ Center |\n| A | B | C |\n| D | E | F |"); + + $table = $doc->getChildren()[0]; + $rows = $table->getChildren(); + + // Header row alignments + $headerCells = $rows[0]->getChildren(); + $this->assertSame(TableCell::ALIGN_RIGHT, $headerCells[0]->getAlignment()); + $this->assertSame(TableCell::ALIGN_LEFT, $headerCells[1]->getAlignment()); + $this->assertSame(TableCell::ALIGN_CENTER, $headerCells[2]->getAlignment()); + + // Data rows should inherit column alignment from header + $dataCells1 = $rows[1]->getChildren(); + $this->assertSame(TableCell::ALIGN_RIGHT, $dataCells1[0]->getAlignment()); + $this->assertSame(TableCell::ALIGN_LEFT, $dataCells1[1]->getAlignment()); + $this->assertSame(TableCell::ALIGN_CENTER, $dataCells1[2]->getAlignment()); + + $dataCells2 = $rows[2]->getChildren(); + $this->assertSame(TableCell::ALIGN_RIGHT, $dataCells2[0]->getAlignment()); + $this->assertSame(TableCell::ALIGN_LEFT, $dataCells2[1]->getAlignment()); + $this->assertSame(TableCell::ALIGN_CENTER, $dataCells2[2]->getAlignment()); + } + + public function testParseTableSeparatorRowOverridesHeaderAlignment(): void + { + // Separator row alignment takes precedence over header |= alignment + $doc = $this->parser->parse("|=> Right |=< Left |\n|:--------|------:|\n| A | B |"); + + $table = $doc->getChildren()[0]; + $rows = $table->getChildren(); + + // Header cells get alignment from separator row, not from |= markers + $headerCells = $rows[0]->getChildren(); + $this->assertSame(TableCell::ALIGN_LEFT, $headerCells[0]->getAlignment()); + $this->assertSame(TableCell::ALIGN_RIGHT, $headerCells[1]->getAlignment()); + + // Data row also uses separator row alignment + $dataCells = $rows[1]->getChildren(); + $this->assertSame(TableCell::ALIGN_LEFT, $dataCells[0]->getAlignment()); + $this->assertSame(TableCell::ALIGN_RIGHT, $dataCells[1]->getAlignment()); + } + public function testParseBlockAttributes(): void { $doc = $this->parser->parse("{.highlight}\n# Heading");