Merge pull request #767 from aidantwoods/enhancement/header-slug

Create ID's for Header elements so they can be referenced in anchor tags
This commit is contained in:
Aidan Woods 2020-05-10 14:41:15 +01:00 committed by GitHub
commit 0c5e8c152e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 265 additions and 2 deletions

View File

@ -5,6 +5,8 @@ namespace Erusev\Parsedown\Components\Blocks;
use Erusev\Parsedown\AST\Handler; use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\AST\StateRenderable; use Erusev\Parsedown\AST\StateRenderable;
use Erusev\Parsedown\Components\Block; use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Configurables\HeaderSlug;
use Erusev\Parsedown\Configurables\SlugRegister;
use Erusev\Parsedown\Configurables\StrictMode; use Erusev\Parsedown\Configurables\StrictMode;
use Erusev\Parsedown\Html\Renderables\Element; use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Parsedown; use Erusev\Parsedown\Parsedown;
@ -96,9 +98,17 @@ final class Header implements Block
return new Handler( return new Handler(
/** @return Element */ /** @return Element */
function (State $State) { 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( return new Element(
'h' . \strval($this->level()), 'h' . \strval($this->level()),
[], $attributes,
$State->applyTo(Parsedown::line($this->text(), $State)) $State->applyTo(Parsedown::line($this->text(), $State))
); );
} }

View File

@ -6,6 +6,8 @@ use Erusev\Parsedown\AST\Handler;
use Erusev\Parsedown\AST\StateRenderable; use Erusev\Parsedown\AST\StateRenderable;
use Erusev\Parsedown\Components\AcquisitioningBlock; use Erusev\Parsedown\Components\AcquisitioningBlock;
use Erusev\Parsedown\Components\Block; use Erusev\Parsedown\Components\Block;
use Erusev\Parsedown\Configurables\HeaderSlug;
use Erusev\Parsedown\Configurables\SlugRegister;
use Erusev\Parsedown\Html\Renderables\Element; use Erusev\Parsedown\Html\Renderables\Element;
use Erusev\Parsedown\Parsedown; use Erusev\Parsedown\Parsedown;
use Erusev\Parsedown\Parsing\Context; use Erusev\Parsedown\Parsing\Context;
@ -88,9 +90,17 @@ final class SetextHeader implements AcquisitioningBlock
return new Handler( return new Handler(
/** @return Element */ /** @return Element */
function (State $State) { 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( return new Element(
'h' . \strval($this->level()), 'h' . \strval($this->level()),
[], $attributes,
$State->applyTo(Parsedown::line($this->text(), $State)) $State->applyTo(Parsedown::line($this->text(), $State))
); );
} }

View File

@ -0,0 +1,100 @@
<?php
namespace Erusev\Parsedown\Configurables;
use Erusev\Parsedown\Configurable;
final class HeaderSlug implements Configurable
{
/** @var bool */
private $enabled = false;
/** @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,
$duplicationCallback = null
) {
$this->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);
}
}

View File

@ -0,0 +1,44 @@
<?php
namespace Erusev\Parsedown\Configurables;
use Erusev\Parsedown\MutableConfigurable;
final class SlugRegister implements MutableConfigurable
{
/** @var array<string, int> */
private $register;
/**
* @param array<string, int> $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);
}
}

View File

@ -6,6 +6,7 @@ use Erusev\Parsedown\Components\Blocks\Markup as BlockMarkup;
use Erusev\Parsedown\Components\Inlines\Markup as InlineMarkup; use Erusev\Parsedown\Components\Inlines\Markup as InlineMarkup;
use Erusev\Parsedown\Configurables\BlockTypes; use Erusev\Parsedown\Configurables\BlockTypes;
use Erusev\Parsedown\Configurables\Breaks; use Erusev\Parsedown\Configurables\Breaks;
use Erusev\Parsedown\Configurables\HeaderSlug;
use Erusev\Parsedown\Configurables\InlineTypes; use Erusev\Parsedown\Configurables\InlineTypes;
use Erusev\Parsedown\Configurables\SafeMode; use Erusev\Parsedown\Configurables\SafeMode;
use Erusev\Parsedown\Configurables\StrictMode; use Erusev\Parsedown\Configurables\StrictMode;
@ -59,6 +60,7 @@ class ParsedownTest extends TestCase
new SafeMode(\substr($test, 0, 3) === 'xss'), new SafeMode(\substr($test, 0, 3) === 'xss'),
new StrictMode(\substr($test, 0, 6) === 'strict'), new StrictMode(\substr($test, 0, 6) === 'strict'),
new Breaks(\substr($test, 0, 14) === 'breaks_enabled'), new Breaks(\substr($test, 0, 14) === 'breaks_enabled'),
new HeaderSlug(\substr($test, 0, 4) === 'slug'),
])); ]));
$actualMarkup = $Parsedown->toHtml($markdown); $actualMarkup = $Parsedown->toHtml($markdown);

View File

@ -0,0 +1,11 @@
<h1 id="foo">foo</h1>
<h1 id="foo-bar">foo bar</h1>
<h1 id="foobar">foo_bar</h1>
<h1 id="foobar-1">foo+bar-1</h1>
<h1 id="foobar-2">foo+bar</h1>
<h1 id="2rer0ගම්මැද්ද-v-force-ඉනොවේශන්-නේෂන්-සඳහා-එවූ-නි">2rer*(0👍ගම්මැද්ද V FORCE ඉනොවේශන් නේෂන් සඳහා එවූ නි</h1>
<h2 id="foo-1">foo</h2>
<h2 id="foo-bar-1">foo bar</h2>
<h2 id="foobar-3">foo_bar</h2>
<h2 id="foobar-4">foo+bar</h2>
<h2 id="2rer0ගම්මැද්ද-v-force-ඉනොවේශන්-නේෂන්-සඳහා-එවූ-නි-1">2rer*(0👍ගම්මැද්ද V FORCE ඉනොවේශන් නේෂන් සඳහා එවූ නි</h2>

View File

@ -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 ඉනොවේශන් නේෂන් සඳහා එවූ නි
---

View File

@ -0,0 +1,60 @@
<?php
namespace Erusev\Parsedown\Tests\Configurables;
use Erusev\Parsedown\Configurables\HeaderSlug;
use Erusev\Parsedown\Configurables\SlugRegister;
use Erusev\Parsedown\State;
use PHPUnit\Framework\TestCase;
final class HeaderSlugTest extends TestCase
{
/**
* @return void
* @throws \PHPUnit\Framework\ExpectationFailedException
* @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
*/
public function testNamedConstructor()
{
$State = new State([HeaderSlug::enabled()]);
$this->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')
);
}
}