diff --git a/src/Components/Blocks/Header.php b/src/Components/Blocks/Header.php index ae2180c..c3859fe 100644 --- a/src/Components/Blocks/Header.php +++ b/src/Components/Blocks/Header.php @@ -5,6 +5,8 @@ namespace Erusev\Parsedown\Components\Blocks; 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; @@ -96,9 +98,17 @@ final class Header implements Block return new Handler( /** @return Element */ function (State $State) { + $HeaderSlug = $State->get(HeaderSlug::class); + $Register = $State->get(SlugRegister::class); + $attributes = ( + $HeaderSlug->isEnabled() + ? ['id' => $HeaderSlug->transform($Register, $this->text())] + : [] + ); + return new Element( 'h' . \strval($this->level()), - [], + $attributes, $State->applyTo(Parsedown::line($this->text(), $State)) ); } diff --git a/src/Components/Blocks/SetextHeader.php b/src/Components/Blocks/SetextHeader.php index 4e1a5df..482e37a 100644 --- a/src/Components/Blocks/SetextHeader.php +++ b/src/Components/Blocks/SetextHeader.php @@ -6,6 +6,8 @@ use Erusev\Parsedown\AST\Handler; 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; @@ -88,9 +90,17 @@ final class SetextHeader implements AcquisitioningBlock return new Handler( /** @return Element */ function (State $State) { + $HeaderSlug = $State->get(HeaderSlug::class); + $Register = $State->get(SlugRegister::class); + $attributes = ( + $HeaderSlug->isEnabled() + ? ['id' => $HeaderSlug->transform($Register, $this->text())] + : [] + ); + return new Element( 'h' . \strval($this->level()), - [], + $attributes, $State->applyTo(Parsedown::line($this->text(), $State)) ); } diff --git a/src/Configurables/HeaderSlug.php b/src/Configurables/HeaderSlug.php new file mode 100644 index 0000000..a0bc3c3 --- /dev/null +++ b/src/Configurables/HeaderSlug.php @@ -0,0 +1,100 @@ +enabled = $enabled; + + if (! isset($slugCallback)) { + $this->slugCallback = function (string $text): string { + $slug = \mb_strtolower($text); + $slug = \str_replace(' ', '-', $slug); + $slug = \preg_replace('/[^\p{L}\p{Nd}\p{Nl}\p{M}-]+/u', '', $slug); + $slug = \trim($slug, '-'); + + return $slug; + }; + } 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 */ + public function isEnabled() + { + return $this->enabled; + } + + public function transform(SlugRegister $SlugRegister, string $text): string + { + $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 */ + public static function withCallback($slugCallback): self + { + 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() + { + return new self(true); + } + + /** @return self */ + public static function initial() + { + return new self(false); + } +} 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/ParsedownTest.php b/tests/ParsedownTest.php index d463279..f0833c5 100755 --- a/tests/ParsedownTest.php +++ b/tests/ParsedownTest.php @@ -6,6 +6,7 @@ use Erusev\Parsedown\Components\Blocks\Markup as BlockMarkup; use Erusev\Parsedown\Components\Inlines\Markup as InlineMarkup; use Erusev\Parsedown\Configurables\BlockTypes; use Erusev\Parsedown\Configurables\Breaks; +use Erusev\Parsedown\Configurables\HeaderSlug; use Erusev\Parsedown\Configurables\InlineTypes; use Erusev\Parsedown\Configurables\SafeMode; use Erusev\Parsedown\Configurables\StrictMode; @@ -59,6 +60,7 @@ class ParsedownTest extends TestCase new SafeMode(\substr($test, 0, 3) === 'xss'), new StrictMode(\substr($test, 0, 6) === 'strict'), new Breaks(\substr($test, 0, 14) === 'breaks_enabled'), + new HeaderSlug(\substr($test, 0, 4) === 'slug'), ])); $actualMarkup = $Parsedown->toHtml($markdown); diff --git a/tests/data/slug_heading.html b/tests/data/slug_heading.html new file mode 100644 index 0000000..56b6021 --- /dev/null +++ b/tests/data/slug_heading.html @@ -0,0 +1,11 @@ +

foo

+

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 diff --git a/tests/data/slug_heading.md b/tests/data/slug_heading.md new file mode 100644 index 0000000..2317bb5 --- /dev/null +++ b/tests/data/slug_heading.md @@ -0,0 +1,26 @@ +# foo + +# 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 diff --git a/tests/src/Configurables/HeaderSlugTest.php b/tests/src/Configurables/HeaderSlugTest.php new file mode 100644 index 0000000..e831a70 --- /dev/null +++ b/tests/src/Configurables/HeaderSlugTest.php @@ -0,0 +1,60 @@ +assertSame(true, $State->get(HeaderSlug::class)->isEnabled()); + } + + /** + * @return void + * @throws \PHPUnit\Framework\ExpectationFailedException + * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException + */ + public function testCustomCallback() + { + $HeaderSlug = HeaderSlug::withCallback(function (string $t): string { + return \preg_replace('/[^A-Za-z0-9]++/', '_', $t); + }); + + $this->assertSame( + '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') + ); + } +}