diff --git a/src/Components/Blocks/Header.php b/src/Components/Blocks/Header.php index cbdfae1..c3859fe 100644 --- a/src/Components/Blocks/Header.php +++ b/src/Components/Blocks/Header.php @@ -6,6 +6,7 @@ use Erusev\Parsedown\AST\Handler; use Erusev\Parsedown\AST\StateRenderable; use Erusev\Parsedown\Components\Block; use Erusev\Parsedown\Configurables\HeaderSlug; +use Erusev\Parsedown\Configurables\SlugRegister; use Erusev\Parsedown\Configurables\StrictMode; use Erusev\Parsedown\Html\Renderables\Element; use Erusev\Parsedown\Parsedown; @@ -98,9 +99,10 @@ final class Header implements Block /** @return Element */ function (State $State) { $HeaderSlug = $State->get(HeaderSlug::class); + $Register = $State->get(SlugRegister::class); $attributes = ( $HeaderSlug->isEnabled() - ? ['id' => $HeaderSlug->transform($this->text())] + ? ['id' => $HeaderSlug->transform($Register, $this->text())] : [] ); diff --git a/src/Components/Blocks/SetextHeader.php b/src/Components/Blocks/SetextHeader.php index dbdf04b..482e37a 100644 --- a/src/Components/Blocks/SetextHeader.php +++ b/src/Components/Blocks/SetextHeader.php @@ -7,6 +7,7 @@ use Erusev\Parsedown\AST\StateRenderable; use Erusev\Parsedown\Components\AcquisitioningBlock; use Erusev\Parsedown\Components\Block; use Erusev\Parsedown\Configurables\HeaderSlug; +use Erusev\Parsedown\Configurables\SlugRegister; use Erusev\Parsedown\Html\Renderables\Element; use Erusev\Parsedown\Parsedown; use Erusev\Parsedown\Parsing\Context; @@ -90,9 +91,10 @@ final class SetextHeader implements AcquisitioningBlock /** @return Element */ function (State $State) { $HeaderSlug = $State->get(HeaderSlug::class); + $Register = $State->get(SlugRegister::class); $attributes = ( $HeaderSlug->isEnabled() - ? ['id' => $HeaderSlug->transform($this->text())] + ? ['id' => $HeaderSlug->transform($Register, $this->text())] : [] ); diff --git a/src/Configurables/HeaderSlug.php b/src/Configurables/HeaderSlug.php index 2f32699..a0bc3c3 100644 --- a/src/Configurables/HeaderSlug.php +++ b/src/Configurables/HeaderSlug.php @@ -12,12 +12,19 @@ final class HeaderSlug implements Configurable /** @var \Closure(string):string */ private $slugCallback; + /** @var \Closure(string,int):string */ + private $duplicationCallback; + /** * @param bool $enabled * @param (\Closure(string):string)|null $slugCallback + * @param (\Closure(string, int):string)|null $duplicationCallback */ - public function __construct($enabled, $slugCallback = null) - { + public function __construct( + $enabled, + $slugCallback = null, + $duplicationCallback = null + ) { $this->enabled = $enabled; if (! isset($slugCallback)) { @@ -32,6 +39,14 @@ final class HeaderSlug implements Configurable } else { $this->slugCallback = $slugCallback; } + + if (! isset($duplicationCallback)) { + $this->duplicationCallback = function (string $slug, int $duplicateNumber): string { + return $slug . '-' . \strval($duplicateNumber-1); + }; + } else { + $this->duplicationCallback = $duplicationCallback; + } } /** @return bool */ @@ -40,9 +55,23 @@ final class HeaderSlug implements Configurable return $this->enabled; } - public function transform(string $text): string + public function transform(SlugRegister $SlugRegister, string $text): string { - return ($this->slugCallback)($text); + $slug = ($this->slugCallback)($text); + + if ($SlugRegister->slugCount($slug) > 0) { + $newSlug = ($this->duplicationCallback)($slug, $SlugRegister->mutatingIncrement($slug)); + + while ($SlugRegister->slugCount($newSlug) > 0) { + $newSlug = ($this->duplicationCallback)($slug, $SlugRegister->mutatingIncrement($slug)); + } + + return $newSlug; + } + + $SlugRegister->mutatingIncrement($slug); + + return $slug; } /** @param \Closure(string):string $slugCallback */ @@ -51,6 +80,12 @@ final class HeaderSlug implements Configurable return new self(true, $slugCallback); } + /** @param \Closure(string,int):string $duplicationCallback */ + public static function withDuplicationCallback($duplicationCallback): self + { + return new self(true, null, $duplicationCallback); + } + /** @return self */ public static function enabled() { diff --git a/src/Configurables/SlugRegister.php b/src/Configurables/SlugRegister.php new file mode 100644 index 0000000..1a659e0 --- /dev/null +++ b/src/Configurables/SlugRegister.php @@ -0,0 +1,44 @@ + */ + private $register; + + /** + * @param array $register + */ + public function __construct(array $register = []) + { + $this->register = $register; + } + + /** @return self */ + public static function initial() + { + return new self; + } + + public function mutatingIncrement(string $slug): int + { + if (! isset($this->register[$slug])) { + $this->register[$slug] = 0; + } + + return ++$this->register[$slug]; + } + + public function slugCount(string $slug): int + { + return $this->register[$slug] ?? 0; + } + + public function isolatedCopy(): self + { + return new self($this->register); + } +} diff --git a/tests/data/slug_heading.html b/tests/data/slug_heading.html index d379bb4..56b6021 100644 --- a/tests/data/slug_heading.html +++ b/tests/data/slug_heading.html @@ -1,10 +1,11 @@

foo

foo bar

foo_bar

-

foo+bar

+

foo+bar-1

+

foo+bar

2rer*(0👍ගම්මැද්ද V FORCE ඉනොවේශන් නේෂන් සඳහා එවූ නි

-

foo

-

foo bar

-

foo_bar

-

foo+bar

-

2rer*(0👍ගම්මැද්ද V FORCE ඉනොවේශන් නේෂන් සඳහා එවූ නි

\ No newline at end of file +

foo

+

foo bar

+

foo_bar

+

foo+bar

+

2rer*(0👍ගම්මැද්ද V FORCE ඉනොවේශන් නේෂන් සඳහා එවූ නි

\ No newline at end of file diff --git a/tests/data/slug_heading.md b/tests/data/slug_heading.md index 01f66d6..2317bb5 100644 --- a/tests/data/slug_heading.md +++ b/tests/data/slug_heading.md @@ -4,6 +4,8 @@ # foo_bar +# foo+bar-1 + # foo+bar # 2rer*(0👍ගම්මැද්ද V FORCE ඉනොවේශන් නේෂන් සඳහා එවූ නි diff --git a/tests/src/Configurables/HeaderSlugTest.php b/tests/src/Configurables/HeaderSlugTest.php index 7b4806a..e831a70 100644 --- a/tests/src/Configurables/HeaderSlugTest.php +++ b/tests/src/Configurables/HeaderSlugTest.php @@ -3,6 +3,7 @@ namespace Erusev\Parsedown\Tests\Configurables; use Erusev\Parsedown\Configurables\HeaderSlug; +use Erusev\Parsedown\Configurables\SlugRegister; use Erusev\Parsedown\State; use PHPUnit\Framework\TestCase; @@ -19,6 +20,7 @@ final class HeaderSlugTest extends TestCase $this->assertSame(true, $State->get(HeaderSlug::class)->isEnabled()); } + /** * @return void * @throws \PHPUnit\Framework\ExpectationFailedException @@ -32,7 +34,27 @@ final class HeaderSlugTest extends TestCase $this->assertSame( 'foo_bar', - $HeaderSlug->transform('foo bar') + $HeaderSlug->transform(SlugRegister::initial(), 'foo bar') + ); + } + + /** + * @return void + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + */ + public function testCustomDuplicationCallback() + { + $HeaderSlug = HeaderSlug::withDuplicationCallback(function (string $t, int $n): string { + return $t . '_' . \strval($n-1); + }); + + $SlugRegister = new SlugRegister; + $HeaderSlug->transform($SlugRegister, 'foo bar'); + + $this->assertSame( + 'foo-bar_1', + $HeaderSlug->transform($SlugRegister, 'foo bar') ); } }