Skip to content

Commit

Permalink
Merge pull request #939 from MrSpoocy/allow_headingpermlink_attach_he…
Browse files Browse the repository at this point in the history
…ading

Allow headingpermlink attaching to the heading
  • Loading branch information
colinodell authored Oct 30, 2022
2 parents 9a3ec4b + 446fc11 commit fedc9ec
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 17 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) princi
- `MissingDependencyException`
- `NoMatchingRendererException`
- `ParserLogicException`
- Added more configuration options to the Heading Permalinks extension (#939):
- `heading_permalink/apply_id_to_heading` - When `true`, the `id` attribute will be applied to the heading element itself instead of the `<a>` tag
- `heading_permalink/heading_class` - class to apply to the heading element
- `heading_permalink/insert` - now accepts `none` to prevent the creation of the `<a>` link

### Changed

Expand Down
12 changes: 11 additions & 1 deletion docs/2.4/extensions/heading-permalinks.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ $config = [
'heading_permalink' => [
'html_class' => 'heading-permalink',
'id_prefix' => 'content',
'apply_id_to_heading' => false,
'heading_class' => ''
'fragment_prefix' => 'content',
'insert' => 'before',
'min_heading_level' => 1,
Expand Down Expand Up @@ -72,6 +74,14 @@ The value of this nested configuration option should be a `string` that you want

This should be a `string` you want prepended to HTML IDs. This prevents generating HTML ID attributes which might conflict with others in your stylesheet. A dash separator (`-`) will be added between the prefix and the ID. You can instead set this to an empty string (`''`) if you don't want a prefix.

### `apply_id_to_heading`

If this value is `true`, the `id` attributes will be written to the `<h>` tag instead of the `<a>`.

### `heading_class`

The class will be added to the `<h>` tag (no matter if `apply_id_to_heading` is set true or false)

### `fragment_prefix`

This should be a `string` you want prepended to the URL fragment in the link's `href` attribute. **This should typically be set to the same value as `id_prefix` for links to work properly.** However, you may not want to expose that same prefix in your URLs - in that case, you can set this to something different (even an empty string) and use JavaScript to "rewrite" them.
Expand All @@ -94,7 +104,7 @@ if (window.location.hash) {

### `insert`

This controls whether the anchor is added to the beginning of the `<h1>`, `<h2>` etc. tag or to the end. Can be set to either `'before'` or `'after'`.
This controls whether the anchor is added to the beginning of the heading tag (`before`), the end of the tag (`after`), or not added at all (`none`).

### `min_heading_level` and `max_heading_level`

Expand Down
4 changes: 3 additions & 1 deletion src/Extension/HeadingPermalink/HeadingPermalinkExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,10 @@ public function configureSchema(ConfigurationBuilderInterface $builder): void
$builder->addSchema('heading_permalink', Expect::structure([
'min_heading_level' => Expect::int()->min(1)->max(6)->default(1),
'max_heading_level' => Expect::int()->min(1)->max(6)->default(6),
'insert' => Expect::anyOf(HeadingPermalinkProcessor::INSERT_BEFORE, HeadingPermalinkProcessor::INSERT_AFTER)->default(HeadingPermalinkProcessor::INSERT_BEFORE),
'insert' => Expect::anyOf(HeadingPermalinkProcessor::INSERT_BEFORE, HeadingPermalinkProcessor::INSERT_AFTER, HeadingPermalinkProcessor::INSERT_NONE)->default(HeadingPermalinkProcessor::INSERT_BEFORE),
'id_prefix' => Expect::string()->default('content'),
'apply_id_to_heading' => Expect::bool()->default(false),
'heading_class' => Expect::string()->default(''),
'fragment_prefix' => Expect::string()->default('content'),
'html_class' => Expect::string()->default('heading-permalink'),
'title' => Expect::string()->default('Permalink'),
Expand Down
31 changes: 24 additions & 7 deletions src/Extension/HeadingPermalink/HeadingPermalinkProcessor.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ final class HeadingPermalinkProcessor implements EnvironmentAwareInterface
{
public const INSERT_BEFORE = 'before';
public const INSERT_AFTER = 'after';
public const INSERT_NONE = 'none';

/** @psalm-readonly-allow-private-mutation */
private TextNormalizerInterface $slugNormalizer;
Expand All @@ -46,26 +47,40 @@ public function setEnvironment(EnvironmentInterface $environment): void

public function __invoke(DocumentParsedEvent $e): void
{
$min = (int) $this->config->get('heading_permalink/min_heading_level');
$max = (int) $this->config->get('heading_permalink/max_heading_level');

$slugLength = (int) $this->config->get('slug_normalizer/max_length');
$min = (int) $this->config->get('heading_permalink/min_heading_level');
$max = (int) $this->config->get('heading_permalink/max_heading_level');
$applyToHeading = (bool) $this->config->get('heading_permalink/apply_id_to_heading');
$idPrefix = (string) $this->config->get('heading_permalink/id_prefix');
$slugLength = (int) $this->config->get('slug_normalizer/max_length');
$headingClass = (string) $this->config->get('heading_permalink/heading_class');

if ($idPrefix !== '') {
$idPrefix .= '-';
}

foreach ($e->getDocument()->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) {
if ($node instanceof Heading && $node->getLevel() >= $min && $node->getLevel() <= $max) {
$this->addHeadingLink($node, $slugLength);
$this->addHeadingLink($node, $slugLength, $idPrefix, $applyToHeading, $headingClass);
}
}
}

private function addHeadingLink(Heading $heading, int $slugLength): void
private function addHeadingLink(Heading $heading, int $slugLength, string $idPrefix, bool $applyToHeading, string $headingClass): void
{
$text = StringContainerHelper::getChildText($heading, [RawMarkupContainerInterface::class]);
$slug = $this->slugNormalizer->normalize($text, [
'node' => $heading,
'length' => $slugLength,
]);

if ($applyToHeading) {
$heading->data->set('attributes/id', $idPrefix . $slug);
}

if ($headingClass !== '') {
$heading->data->append('attributes/class', $headingClass);
}

$headingLinkAnchor = new HeadingPermalink($slug);

switch ($this->config->get('heading_permalink/insert')) {
Expand All @@ -76,9 +91,11 @@ private function addHeadingLink(Heading $heading, int $slugLength): void
case self::INSERT_AFTER:
$heading->appendChild($headingLinkAnchor);

return;
case self::INSERT_NONE:
return;
default:
throw new InvalidConfigurationException("Invalid configuration value for heading_permalink/insert; expected 'before' or 'after'");
throw new InvalidConfigurationException("Invalid configuration value for heading_permalink/insert; expected 'before', 'after', or 'none'");
}
}
}
20 changes: 13 additions & 7 deletions src/Extension/HeadingPermalink/HeadingPermalinkRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,18 +49,24 @@ public function render(Node $node, ChildNodeRendererInterface $childRenderer): \

$slug = $node->getSlug();

$idPrefix = (string) $this->config->get('heading_permalink/id_prefix');
if ($idPrefix !== '') {
$idPrefix .= '-';
}

$fragmentPrefix = (string) $this->config->get('heading_permalink/fragment_prefix');
if ($fragmentPrefix !== '') {
$fragmentPrefix .= '-';
}

$attrs = $node->data->getData('attributes');
$attrs->set('id', $idPrefix . $slug);
$attrs = $node->data->getData('attributes');
$appendId = ! $this->config->get('heading_permalink/apply_id_to_heading');

if ($appendId) {
$idPrefix = (string) $this->config->get('heading_permalink/id_prefix');

if ($idPrefix !== '') {
$idPrefix .= '-';
}

$attrs->set('id', $idPrefix . $slug);
}

$attrs->set('href', '#' . $fragmentPrefix . $slug);
$attrs->append('class', $this->config->get('heading_permalink/html_class'));

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkExtension;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkProcessor;
use League\CommonMark\Extension\HeadingPermalink\HeadingPermalinkRenderer;
use League\CommonMark\MarkdownConverter;
use League\CommonMark\Parser\MarkdownParser;
Expand Down Expand Up @@ -60,7 +61,7 @@ public function testHeadingPermalinksWithCustomOptions(string $input, string $ex
'fragment_prefix' => 'custom-fragment-prefix',
// Ensure multiple characters are allowed (including multibyte) and special HTML characters are escaped.
'symbol' => '¶ 🦄️ <3 You',
'insert' => 'after',
'insert' => HeadingPermalinkProcessor::INSERT_AFTER,
'title' => 'Link',
'aria_hidden' => false,
],
Expand Down Expand Up @@ -164,6 +165,63 @@ public function testWithCustomLevels(): void
$this->assertEquals($expected, \trim((string) $converter->convert($input)));
}

public function testHeadingPermalinksWithApplyIdToHeading(): void
{
$environment = new Environment([
'heading_permalink' => [
'apply_id_to_heading' => true,
],
]);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new HeadingPermalinkExtension());

$converter = new MarkdownConverter($environment);

$input = '# Hello World!';
$expected = \sprintf('<h1 id="content-hello-world"><a href="#content-hello-world" class="heading-permalink" aria-hidden="true" title="Permalink">%s</a>Hello World!</h1>', HeadingPermalinkRenderer::DEFAULT_SYMBOL);

$this->assertEquals($expected, \trim((string) $converter->convert($input)));
}

public function testHeadingPermalinksWithApplyIdToHeadingAndClass(): void
{
$environment = new Environment([
'heading_permalink' => [
'apply_id_to_heading' => true,
'heading_class' => 'heading-anchor',
],
]);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new HeadingPermalinkExtension());

$converter = new MarkdownConverter($environment);

$input = '# Hello World!';
$expected = \sprintf('<h1 id="content-hello-world" class="heading-anchor"><a href="#content-hello-world" class="heading-permalink" aria-hidden="true" title="Permalink">%s</a>Hello World!</h1>', HeadingPermalinkRenderer::DEFAULT_SYMBOL);

$this->assertEquals($expected, \trim((string) $converter->convert($input)));
}

public function testHeadingPermalinksWithApplyIdToHeadingWithoutLink(): void
{
$environment = new Environment([
'heading_permalink' => [
'insert' => HeadingPermalinkProcessor::INSERT_NONE,
'apply_id_to_heading' => true,
'heading_class' => 'heading-anchor',
],
]);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new HeadingPermalinkExtension());

$converter = new MarkdownConverter($environment);

$input = '# Hello World!';
$expected = '<h1 id="content-hello-world" class="heading-anchor">Hello World!</h1>';

$this->assertEquals($expected, \trim((string) $converter->convert($input)));
}

public function testXml(): void
{
$md = '# Hello *World*';
Expand Down

0 comments on commit fedc9ec

Please sign in to comment.