respect-validation/src-dev/Markdown/Content.php
Henrique Moody 7c681fec66
Fix SPDX headers in all files
I ran the `bin/console spdx --fix` with different strategies for
different files. For most of the core classes, since they've been
drastically rebuilt, I've run it with the `git-blame` strategy, for for
the `src/Validators`, in which the API changed completely but the logic
remains the same, I use the `git-log` strategy.
2026-02-03 15:23:23 +01:00

312 lines
9.2 KiB
PHP

<?php
/*
* SPDX-License-Identifier: MIT
* SPDX-FileCopyrightText: (c) Respect Project Contributors
* SPDX-FileContributor: Alexandre Gomes Gaigalas <alganet@gmail.com>
* SPDX-FileContributor: Henrique Moody <henriquemoody@gmail.com>
*/
declare(strict_types=1);
namespace Respect\Dev\Markdown;
use ArrayIterator;
use IteratorAggregate;
use UnexpectedValueException;
use function array_fill;
use function array_find_key;
use function array_keys;
use function array_map;
use function array_slice;
use function count;
use function explode;
use function file_get_contents;
use function implode;
use function max;
use function preg_match;
use function preg_replace;
use function reset;
use function rtrim;
use function sprintf;
use function str_pad;
use function str_repeat;
use function str_replace;
use function str_starts_with;
use function strlen;
use function strrpos;
use function trim;
use const PHP_EOL;
use const STR_PAD_BOTH;
use const STR_PAD_LEFT;
use const STR_PAD_RIGHT;
final class Content implements IteratorAggregate
{
public const string REFERENCES_SECTION = '/^\[.+\]: .+$/';
public function __construct(
/** @var array<string> */
private array $lines = [],
) {
}
public static function from(string $filename): self
{
return new self(explode(PHP_EOL, file_get_contents($filename)));
}
public function build(): string
{
return trim(implode(PHP_EOL, $this->lines)) . PHP_EOL;
}
public function raw(string ...$lines): void
{
$this->lines = [...$this->lines, ...$lines];
}
public function paragraph(string $text): void
{
$this->lines[] = $text;
}
public function emptyLine(): void
{
$this->lines[] = '';
}
public function hr(): void
{
$this->lines[] = '---' . PHP_EOL;
}
public function heading(string $title, int $level): void
{
$this->lines[] = str_repeat('#', $level) . ' ' . $title . PHP_EOL;
}
public function h1(string $title): void
{
$this->heading($title, 1);
}
public function h2(string $title): void
{
$this->heading($title, 2);
}
public function h3(string $title): void
{
$this->heading($title, 3);
}
public function listItem(string $item): void
{
$this->lines[] = '- ' . $item;
}
public function anchorListItem(string $title, string $href): void
{
$this->listItem(sprintf('[%s](%s)', $title, $href));
}
public function reference(string $title, string $href, string|null $description = null): void
{
$this->lines[] = match ($description) {
null => sprintf('[%s]: %s', $title, $href),
default => sprintf('[%s]: %s "%s"', $title, $href, $description),
};
}
public function extractSpdx(): self
{
$start = 0;
$end = 0;
foreach ($this->lines as $position => $line) {
if (preg_match('/^<!--/', $line) === 1) {
$start = $position;
continue;
}
if (preg_match('/-->/', $line) === 1) {
$end = $position;
break;
}
}
return new self(array_slice($this->lines, $start, $end + 2));
}
public static function stripRefs(string $text): string
{
return preg_replace('/\[(.+?)\](?:\[\]|\(.+?\))/', '$1', $text) ?? $text;
}
public function withSection(Content $content): self
{
$firstLine = reset($content->lines);
// Strip trailing whitespace/newlines for comparison
$firstLineTrimmed = rtrim($firstLine);
$sectionStart = array_find_key($this->lines, static fn($line) => $line === $firstLineTrimmed);
// If exact match not found, try comparing trimmed versions
if ($sectionStart === null) {
$sectionStart = array_find_key($this->lines, static fn($line) => rtrim($line) === $firstLineTrimmed);
}
if ($sectionStart === null) {
return new self([...$this->lines, ...$content->lines]);
}
$headingLevel = str_starts_with($firstLine, '#') ? strrpos($firstLine, '#') + 1 : 0;
$sectionEnd = count($this->lines) - 1;
for ($index = $sectionStart + 1; $index < count($this->lines); $index++) {
$currentLine = $this->lines[$index];
if (
$headingLevel === 0
&& (str_starts_with($currentLine, '#') || preg_match(self::REFERENCES_SECTION, $currentLine) === 1)
) {
$sectionEnd = $index;
break;
}
if (
($headingLevel > 0 && str_starts_with($currentLine, str_repeat('#', $headingLevel) . ' '))
|| preg_match(self::REFERENCES_SECTION, $currentLine) === 1
) {
$sectionEnd = $index;
break;
}
}
$before = array_slice($this->lines, 0, $sectionStart);
$after = array_slice($this->lines, $sectionEnd);
return new self([...$before, ...$content->lines, ...$after]);
}
public function getSection(string $text): self
{
$sectionStart = array_find_key($this->lines, static fn($line) => $line === $text);
if ($sectionStart === null) {
throw new UnexpectedValueException('Section not found: ' . $text . ': ' . implode(', ', $this->lines));
}
$headingLevel = str_starts_with($text, '#') ? strrpos($text, '#') + 1 : 0;
$sectionEnd = count($this->lines);
for ($index = $sectionStart + 1; $index < count($this->lines); $index++) {
$currentLine = $this->lines[$index];
if (
$headingLevel === 0
&& (str_starts_with($currentLine, '#') || preg_match(self::REFERENCES_SECTION, $currentLine) === 1)
) {
$sectionEnd = $index;
break;
}
if (
str_starts_with($currentLine, str_repeat('#', $headingLevel) . ' ')
|| preg_match(self::REFERENCES_SECTION, $currentLine) === 1
) {
$sectionEnd = $index;
break;
}
}
return new self(array_slice($this->lines, $sectionStart, $sectionEnd - $sectionStart));
}
/**
* @param array<int, string> $headers
* @param array<int, array<int, string>> $rows
* @param array<int, int> $alignment
*/
public function table(array $headers, array $rows, array $alignment = []): void
{
$lengths = [];
$alignment = $alignment ?: array_fill(0, count($headers), 0);
$filteredHeaders = [];
foreach ($headers as $key => $header) {
$filteredHeader = $this->formatTableCell($header);
$filteredHeaders[$key] = $filteredHeader;
$lengths[$key] ??= 0;
$lengths[$key] = max($lengths[$key], strlen($filteredHeader));
}
$filteredRows = [];
foreach ($rows as $rowIndex => $row) {
$filteredRows[$rowIndex] = [];
foreach ($row as $key => $cell) {
$filteredCell = $this->formatTableCell($cell);
$filteredRows[$rowIndex][$key] = $filteredCell;
$lengths[$key] ??= 0;
$lengths[$key] = max($lengths[$key], strlen($filteredCell));
}
}
$this->lines[] = $this->formatTableRow($filteredHeaders, $lengths, $alignment);
$this->lines[] = $this->formatTableRow(
array_map(
static function (int $key) use ($lengths, $alignment): string {
$length = $lengths[$key];
return match ($alignment[$key] ?? 0) {
-1 => ':' . str_repeat('-', $length - 1),
1 => str_repeat('-', $length - 1) . ':',
default => str_repeat('-', $length),
};
},
array_keys($headers),
),
$lengths,
$alignment,
);
$this->lines = [
...$this->lines,
...array_map(fn(array $row) => $this->formatTableRow($row, $lengths, $alignment), $filteredRows),
'',
];
}
public function getIterator(): ArrayIterator
{
return new ArrayIterator($this->lines);
}
/** @return array<string> */
public function toArray(): array
{
return $this->lines;
}
private function formatTableCell(string $value): string
{
return str_replace('|', '&#124;', $value);
}
/**
* @param array<int, string> $row
* @param array<int, int> $lengths
* @param array<int, int> $alignment
*/
private function formatTableRow(array $row, array $lengths, array $alignment): string
{
$cells = [];
foreach ($row as $key => $cell) {
$cells[] = match ($alignment[$key] ?? 0) {
-1 => str_pad($cell, $lengths[$key], ' ', STR_PAD_RIGHT),
1 => str_pad($cell, $lengths[$key], ' ', STR_PAD_LEFT),
default => str_pad($cell, $lengths[$key], ' ', STR_PAD_BOTH),
};
}
return '| ' . implode(' | ', $cells) . ' |';
}
}