├── assets ├── page-toc.png ├── page-toc-anchors.js └── page-toc-anchors.css ├── vendor ├── composer │ ├── tmp-f711f3efca8baf836db7f0f0b18aef94~ │ ├── autoload_namespaces.php │ ├── autoload_classmap.php │ ├── autoload_psr4.php │ ├── platform_check.php │ ├── LICENSE │ ├── autoload_real.php │ ├── installed.php │ ├── autoload_static.php │ └── installed.json ├── knplabs │ └── knp-menu │ │ ├── src │ │ └── Knp │ │ │ └── Menu │ │ │ ├── Resources │ │ │ └── views │ │ │ │ ├── knp_menu_base.html.twig │ │ │ │ ├── knp_menu_ordered.html.twig │ │ │ │ └── knp_menu.html.twig │ │ │ ├── FactoryInterface.php │ │ │ ├── Loader │ │ │ ├── LoaderInterface.php │ │ │ ├── NodeLoader.php │ │ │ └── ArrayLoader.php │ │ │ ├── Matcher │ │ │ ├── Voter │ │ │ │ ├── VoterInterface.php │ │ │ │ ├── UriVoter.php │ │ │ │ ├── CallbackVoter.php │ │ │ │ ├── RegexVoter.php │ │ │ │ └── RouteVoter.php │ │ │ ├── MatcherInterface.php │ │ │ └── Matcher.php │ │ │ ├── Renderer │ │ │ ├── RendererProviderInterface.php │ │ │ ├── RendererInterface.php │ │ │ ├── PsrProvider.php │ │ │ ├── ArrayAccessProvider.php │ │ │ ├── TwigRenderer.php │ │ │ ├── Renderer.php │ │ │ └── ListRenderer.php │ │ │ ├── Iterator │ │ │ ├── DisplayedItemFilterIterator.php │ │ │ ├── CurrentItemFilterIterator.php │ │ │ └── RecursiveItemIterator.php │ │ │ ├── Provider │ │ │ ├── MenuProviderInterface.php │ │ │ ├── PsrProvider.php │ │ │ ├── ChainProvider.php │ │ │ ├── ArrayAccessProvider.php │ │ │ └── LazyProvider.php │ │ │ ├── Factory │ │ │ ├── ExtensionInterface.php │ │ │ └── CoreExtension.php │ │ │ ├── NodeInterface.php │ │ │ ├── Integration │ │ │ └── Symfony │ │ │ │ └── RoutingExtension.php │ │ │ ├── MenuFactory.php │ │ │ ├── Twig │ │ │ ├── MenuExtension.php │ │ │ └── Helper.php │ │ │ ├── ItemInterface.php │ │ │ └── Util │ │ │ └── MenuManipulator.php │ │ ├── LICENSE │ │ ├── composer.json │ │ └── CHANGELOG.md ├── cocur │ └── slugify │ │ ├── src │ │ ├── RuleProvider │ │ │ ├── RuleProviderInterface.php │ │ │ └── FileRuleProvider.php │ │ ├── Bridge │ │ │ ├── ZF2 │ │ │ │ ├── SlugifyViewHelperFactory.php │ │ │ │ ├── SlugifyService.php │ │ │ │ ├── SlugifyViewHelper.php │ │ │ │ └── Module.php │ │ │ ├── Latte │ │ │ │ └── SlugifyHelper.php │ │ │ ├── Symfony │ │ │ │ ├── CocurSlugifyBundle.php │ │ │ │ ├── Configuration.php │ │ │ │ └── CocurSlugifyExtension.php │ │ │ ├── Laravel │ │ │ │ ├── SlugifyFacade.php │ │ │ │ └── SlugifyServiceProvider.php │ │ │ ├── League │ │ │ │ └── SlugifyServiceProvider.php │ │ │ ├── Plum │ │ │ │ └── SlugifyConverter.php │ │ │ ├── Nette │ │ │ │ └── SlugifyExtension.php │ │ │ └── Twig │ │ │ │ └── SlugifyExtension.php │ │ ├── SlugifyInterface.php │ │ └── Slugify.php │ │ ├── LICENSE │ │ └── composer.json └── autoload.php ├── composer.json ├── templates └── components │ └── page-toc.html.twig ├── LICENSE ├── page-toc.yaml ├── classes ├── shortcodes │ └── AnchorShortcode.php ├── UniqueSlugify.php ├── OrderedListRenderer.php ├── MarkupFixer.php ├── HtmlHelper.php └── TocGenerator.php ├── CHANGELOG.md ├── blueprints.yaml ├── blueprints └── page-toc.yaml ├── composer.lock ├── languages.yaml ├── page-toc.php └── README.md /assets/page-toc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trilbymedia/grav-plugin-page-toc/HEAD/assets/page-toc.png -------------------------------------------------------------------------------- /vendor/composer/tmp-f711f3efca8baf836db7f0f0b18aef94~: -------------------------------------------------------------------------------- 1 | {"message":"Bad credentials","documentation_url":"https://docs.github.com/rest"} -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Resources/views/knp_menu_base.html.twig: -------------------------------------------------------------------------------- 1 | {% if options.compressed %}{{ block('compressed_root') }}{% else %}{{ block('root') }}{% endif %} 2 | -------------------------------------------------------------------------------- /vendor/composer/autoload_namespaces.php: -------------------------------------------------------------------------------- 1 | $vendorDir . '/composer/InstalledVersions.php', 10 | 'Grav\\Plugin\\PageTOCPlugin' => $baseDir . '/page-toc.php', 11 | ); 12 | -------------------------------------------------------------------------------- /assets/page-toc-anchors.js: -------------------------------------------------------------------------------- 1 | document.body.addEventListener('click', (event) => { 2 | if (typeof event.target.dataset.anchorIcon !== 'undefined') { 3 | const href = event.target.href; 4 | navigator.clipboard.writeText(href) 5 | .then(() => {}) 6 | .catch((error) => { 7 | console.error('Unable to copy to clipboard the anchor', error); 8 | }); 9 | } 10 | }, true); -------------------------------------------------------------------------------- /vendor/composer/autoload_psr4.php: -------------------------------------------------------------------------------- 1 | array($vendorDir . '/knplabs/knp-menu/src/Knp/Menu'), 10 | 'Grav\\Plugin\\PageToc\\' => array($baseDir . '/classes'), 11 | 'Cocur\\Slugify\\' => array($vendorDir . '/cocur/slugify/src'), 12 | ); 13 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/FactoryInterface.php: -------------------------------------------------------------------------------- 1 | $options 14 | */ 15 | public function createItem(string $name, array $options = []): ItemInterface; 16 | } 17 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Resources/views/knp_menu_ordered.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'knp_menu.html.twig' %} 2 | 3 | {% block list %} 4 | {% import 'knp_menu.html.twig' as macros %} 5 | 6 | {% if item.hasChildren and options.depth is not same as(0) and item.displayChildren %} 7 | 8 | {{ block('children') }} 9 | 10 | {% endif %} 11 | {% endblock %} 12 | -------------------------------------------------------------------------------- /assets/page-toc-anchors.css: -------------------------------------------------------------------------------- 1 | .toc-anchor { 2 | transition: hover 0.5s ease; 3 | position: absolute; 4 | opacity: 0; 5 | } 6 | 7 | .toc-anchor.after { 8 | margin-left: 5px; 9 | } 10 | 11 | .toc-anchor.before { 12 | margin-left: -22px; 13 | padding-right: 8px; 14 | } 15 | 16 | .toc-anchor:after { 17 | content: attr(data-anchor-icon); 18 | } 19 | 20 | :hover > .toc-anchor, .toc-anchor:focus { 21 | opacity: .5; 22 | } -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Loader/LoaderInterface.php: -------------------------------------------------------------------------------- 1 | uri || null === $item->getUri()) { 19 | return null; 20 | } 21 | 22 | if ($item->getUri() === $this->uri) { 23 | return true; 24 | } 25 | 26 | return null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Iterator/DisplayedItemFilterIterator.php: -------------------------------------------------------------------------------- 1 | current()->isDisplayed(); 17 | } 18 | 19 | /** 20 | * @return bool 21 | */ 22 | #[\ReturnTypeWillChange] 23 | public function hasChildren() 24 | { 25 | return $this->current()->getDisplayChildren() && parent::hasChildren(); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Matcher/Voter/CallbackVoter.php: -------------------------------------------------------------------------------- 1 | getExtra('match_callback'); 15 | 16 | if (null === $callback) { 17 | return null; 18 | } 19 | 20 | if (!\is_callable($callback)) { 21 | throw new \InvalidArgumentException('Extra "match_callback" must be callable.'); 22 | } 23 | 24 | return $callback(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Provider/MenuProviderInterface.php: -------------------------------------------------------------------------------- 1 | $options 13 | * 14 | * @throws \InvalidArgumentException if the menu does not exists 15 | */ 16 | public function get(string $name, array $options = []): ItemInterface; 17 | 18 | /** 19 | * Checks whether a menu exists in this provider 20 | * 21 | * @param array $options 22 | */ 23 | public function has(string $name, array $options = []): bool; 24 | } 25 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Factory/ExtensionInterface.php: -------------------------------------------------------------------------------- 1 | $options The options processed by the previous extensions 13 | * 14 | * @return array 15 | */ 16 | public function buildOptions(array $options): array; 17 | 18 | /** 19 | * Configures the item with the passed options 20 | * 21 | * @param array $options 22 | */ 23 | public function buildItem(ItemInterface $item, array $options): void; 24 | } 25 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trilbymedia/page-toc", 3 | "type": "grav-plugin", 4 | "description": "Page TOC plugin", 5 | "keywords": ["plugin"], 6 | "homepage": "https://github.com/trilbymedia/grav-plugin-page-toc", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "Trilby Media", 11 | "email": "hello@trilby.media", 12 | "role": "Developer" 13 | } 14 | ], 15 | "require": { 16 | "cocur/slugify": "^4.6", 17 | "knplabs/knp-menu": "^3.2", 18 | "php": ">=8.0" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "Grav\\Plugin\\PageToc\\": "classes/" 23 | }, 24 | "classmap": ["page-toc.php"] 25 | }, 26 | "config": { 27 | "platform": { 28 | "php": "8.2.0" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Matcher/MatcherInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Cocur\Slugify\RuleProvider; 13 | 14 | /** 15 | * RuleProviderInterface 16 | * 17 | * @package Cocur\Slugify\RuleProvider 18 | * @author Florian Eckerstorfer 19 | * @copyright 2015 Florian Eckerstorfer 20 | */ 21 | interface RuleProviderInterface 22 | { 23 | /** 24 | * @param $ruleset 25 | * 26 | * @return array 27 | */ 28 | public function getRules(string $ruleset): array; 29 | } 30 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Bridge/ZF2/SlugifyViewHelperFactory.php: -------------------------------------------------------------------------------- 1 | getServiceLocator()->get(Slugify::class); 25 | 26 | return new SlugifyViewHelper($slugify); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Iterator/CurrentItemFilterIterator.php: -------------------------------------------------------------------------------- 1 | $iterator 14 | */ 15 | public function __construct(\Iterator $iterator, private MatcherInterface $matcher) 16 | { 17 | 18 | parent::__construct($iterator); 19 | } 20 | 21 | /** 22 | * @return bool 23 | */ 24 | #[\ReturnTypeWillChange] 25 | public function accept() 26 | { 27 | return $this->matcher->isCurrent($this->current()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/NodeInterface.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function getOptions(): array; 23 | 24 | /** 25 | * Get the child nodes implementing NodeInterface 26 | * 27 | * @return \Traversable 28 | */ 29 | public function getChildren(): \Traversable; 30 | } 31 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Matcher/Voter/RegexVoter.php: -------------------------------------------------------------------------------- 1 | regexp || null === $item->getUri()) { 20 | return null; 21 | } 22 | 23 | if (\preg_match($this->regexp, $item->getUri())) { 24 | return true; 25 | } 26 | 27 | return null; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /vendor/autoload.php: -------------------------------------------------------------------------------- 1 | get('Config'); 24 | 25 | $options = isset($config[Module::CONFIG_KEY]['options']) ? $config[Module::CONFIG_KEY]['options'] : []; 26 | $provider = isset($config[Module::CONFIG_KEY]['provider']) ? $config[Module::CONFIG_KEY]['provider'] : null; 27 | 28 | return new Slugify($options, $provider); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /templates/components/page-toc.html.twig: -------------------------------------------------------------------------------- 1 | 2 | {% macro toc_loop(items) %} 3 | 4 | {% for item in items %} 5 | {% set class = loop.first ? 'first' : loop.last ? 'last' : null %} 6 |
  • 7 | {{ item.label | raw }} 8 | {% if item.children|length > 0 %} 9 |
      10 | {{ _self.toc_loop(item.children) }} 11 |
    12 | {% endif %} 13 |
  • 14 | {% endfor %} 15 | {% endmacro %} 16 | 17 | {% if active or toc_config_var('active') %} 18 |
    19 | {% set table_of_contents = toc_items(page.content) %} 20 | {% if table_of_contents is not empty %} 21 |

    {{ 'PLUGIN_PAGE_TOC.TABLE_OF_CONTENTS'|t }}

    22 |
      23 | {{ _self.toc_loop(table_of_contents.children) }} 24 |
    25 | {% endif %} 26 |
    27 | {% endif %} -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Bridge/Latte/SlugifyHelper.php: -------------------------------------------------------------------------------- 1 | 13 | * @license http://www.opensource.org/licenses/MIT The MIT License 14 | */ 15 | class SlugifyHelper 16 | { 17 | /** @var SlugifyInterface */ 18 | private $slugify; 19 | 20 | /** 21 | * @codeCoverageIgnore 22 | */ 23 | public function __construct(SlugifyInterface $slugify) 24 | { 25 | $this->slugify = $slugify; 26 | } 27 | 28 | /** 29 | * @param string $string 30 | * @param string|null $separator 31 | * 32 | * @return string 33 | */ 34 | public function slugify($string, $separator = null): string 35 | { 36 | return $this->slugify->slugify($string, $separator); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Bridge/Symfony/CocurSlugifyBundle.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Cocur\Slugify\Bridge\Symfony; 13 | 14 | use Symfony\Component\DependencyInjection\Extension\ExtensionInterface; 15 | use Symfony\Component\HttpKernel\Bundle\Bundle; 16 | 17 | /** 18 | * CocurSlugifyBundle 19 | * 20 | * @package cocur/slugify 21 | * @subpackage bridge 22 | * @author Florian Eckerstorfer 23 | * @copyright 2012-2014 Florian Eckerstorfer 24 | * @license http://www.opensource.org/licenses/MIT The MIT License 25 | */ 26 | class CocurSlugifyBundle extends Bundle 27 | { 28 | public function getContainerExtension(): ExtensionInterface 29 | { 30 | return new CocurSlugifyExtension(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Renderer/RendererInterface.php: -------------------------------------------------------------------------------- 1 | $options some rendering options 25 | */ 26 | public function render(ItemInterface $item, array $options = []): string; 27 | } 28 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Loader/NodeLoader.php: -------------------------------------------------------------------------------- 1 | factory->createItem($data->getName(), $data->getOptions()); 22 | 23 | foreach ($data->getChildren() as $childNode) { 24 | $item->addChild($this->load($childNode)); 25 | } 26 | 27 | return $item; 28 | } 29 | 30 | public function supports($data): bool 31 | { 32 | return $data instanceof NodeInterface; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/SlugifyInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Cocur\Slugify; 13 | 14 | /** 15 | * SlugifyInterface 16 | * 17 | * @package org.cocur.slugify 18 | * @author Florian Eckerstorfer 19 | * @author Marchenko Alexandr 20 | * @copyright 2012-2014 Florian Eckerstorfer 21 | * @license http://www.opensource.org/licenses/MIT The MIT License 22 | */ 23 | interface SlugifyInterface 24 | { 25 | /** 26 | * Return a URL safe version of a string. 27 | * 28 | * @param string $string 29 | * @param string|array|null $options 30 | * 31 | * @return string 32 | * 33 | * @api 34 | */ 35 | public function slugify(string $string, array|string|null $options = null): string; 36 | } 37 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Iterator/RecursiveItemIterator.php: -------------------------------------------------------------------------------- 1 | > 9 | * 10 | * @implements \RecursiveIterator> 11 | */ 12 | class RecursiveItemIterator extends \IteratorIterator implements \RecursiveIterator 13 | { 14 | /** 15 | * @param \Traversable $iterator 16 | */ 17 | final public function __construct(\Traversable $iterator) 18 | { 19 | parent::__construct($iterator); 20 | } 21 | 22 | public function hasChildren(): bool 23 | { 24 | return 0 < \count($this->current()); 25 | } 26 | 27 | /** 28 | * @return RecursiveItemIterator 29 | */ 30 | #[\ReturnTypeWillChange] 31 | public function getChildren() 32 | { 33 | return new static($this->current()); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /vendor/composer/platform_check.php: -------------------------------------------------------------------------------- 1 | = 80100)) { 8 | $issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.'; 9 | } 10 | 11 | if ($issues) { 12 | if (!headers_sent()) { 13 | header('HTTP/1.1 500 Internal Server Error'); 14 | } 15 | if (!ini_get('display_errors')) { 16 | if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') { 17 | fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL); 18 | } elseif (!headers_sent()) { 19 | echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL; 20 | } 21 | } 22 | trigger_error( 23 | 'Composer detected issues in your platform: ' . implode(' ', $issues), 24 | E_USER_ERROR 25 | ); 26 | } 27 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Bridge/ZF2/SlugifyViewHelper.php: -------------------------------------------------------------------------------- 1 | slugify = $slugify; 29 | } 30 | 31 | /** 32 | * @param string $string 33 | * @param string|null $separator 34 | * 35 | * @return string 36 | */ 37 | public function __invoke(string $string, string $separator = null) 38 | { 39 | return $this->slugify->slugify($string, $separator); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Bridge/Laravel/SlugifyFacade.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Cocur\Slugify\Bridge\Laravel; 13 | 14 | use Illuminate\Support\Facades\Facade; 15 | 16 | /** 17 | * SlugifyFacade 18 | * 19 | * @package cocur/slugify 20 | * @subpackage bridge 21 | * @author Florian Eckerstorfer 22 | * @author Colin Viebrock 23 | * @copyright 2012-2014 Florian Eckerstorfer 24 | * @license http://www.opensource.org/licenses/MIT The MIT License 25 | */ 26 | class SlugifyFacade extends Facade 27 | { 28 | /** 29 | * Get the registered name of the component. 30 | * 31 | * @return string 32 | * 33 | * @codeCoverageIgnore 34 | */ 35 | protected static function getFacadeAccessor(): string 36 | { 37 | return 'slugify'; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Provider/PsrProvider.php: -------------------------------------------------------------------------------- 1 | container->has($name)) { 23 | throw new \InvalidArgumentException(\sprintf('The menu "%s" is not defined.', $name)); 24 | } 25 | 26 | return $this->container->get($name); 27 | } 28 | 29 | public function has(string $name, array $options = []): bool 30 | { 31 | return $this->container->has($name); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2012-2017 Florian Eckerstorfer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Team Grav 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /vendor/composer/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Copyright (c) Nils Adermann, Jordi Boggiano 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy 5 | of this software and associated documentation files (the "Software"), to deal 6 | in the Software without restriction, including without limitation the rights 7 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the Software is furnished 9 | to do so, subject to the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be included in all 12 | copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | THE SOFTWARE. 21 | 22 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011-present KnpLabs - https://knplabs.com 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Provider/ChainProvider.php: -------------------------------------------------------------------------------- 1 | providers = $providers; 20 | } 21 | 22 | public function get(string $name, array $options = []): ItemInterface 23 | { 24 | foreach ($this->providers as $provider) { 25 | if ($provider->has($name, $options)) { 26 | return $provider->get($name, $options); 27 | } 28 | } 29 | 30 | throw new \InvalidArgumentException(\sprintf('The menu "%s" is not defined.', $name)); 31 | } 32 | 33 | public function has(string $name, array $options = []): bool 34 | { 35 | foreach ($this->providers as $provider) { 36 | if ($provider->has($name, $options)) { 37 | return true; 38 | } 39 | } 40 | 41 | return false; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /page-toc.yaml: -------------------------------------------------------------------------------- 1 | enabled: true # Plugin enabled 2 | include_css: true # Include CSS 3 | active: true # Anchor IDs processed and generated for all pages 4 | templates: # Templates for which anchors should be generated if default is disabled 5 | start: 1 # Start header tag level (1 = h1) for TOC 6 | depth: 6 # Depth from start (2 = 2 levels deep) for TOC 7 | hclass: # Custom Header TOC styling classes 8 | anchors: # Anchor configuration 9 | start: 1 # Start header tag level (1 = h1) 10 | depth: 6 # Depth from start (2 = 2 levels deep) 11 | link: true # Enabled auto-generation of clickable link with fragment 12 | aria: Anchor # Aria label to use 13 | class: # Custom Header anchor styling classes 14 | icon: '#' # Icon to use, can be a symbol, emoji, ascii etc. 15 | position: after # Position to put the anchor, `before|after` 16 | copy_to_clipboard: false # Copy to clipboard functionality 17 | slug_maxlen: 25 # Max length of slugs used for anchors 18 | slug_prefix: # A prefix used in front of generated slugs 19 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/RuleProvider/FileRuleProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Cocur\Slugify\RuleProvider; 13 | 14 | /** 15 | * FileRuleProvider 16 | * 17 | * @package Cocur\Slugify\RuleProvider 18 | * @author Florian Eckerstorfer 19 | * @copyright 2015 Florian Eckerstorfer 20 | */ 21 | class FileRuleProvider implements RuleProviderInterface 22 | { 23 | /** 24 | * @var string 25 | */ 26 | protected string $directoryName; 27 | 28 | /** 29 | * @param string $directoryName 30 | */ 31 | public function __construct(string $directoryName) 32 | { 33 | $this->directoryName = $directoryName; 34 | } 35 | 36 | /** 37 | * @param string $ruleset 38 | * 39 | * @return array 40 | */ 41 | public function getRules(string $ruleset): array 42 | { 43 | $fileName = $this->directoryName . DIRECTORY_SEPARATOR . $ruleset . '.json'; 44 | 45 | return json_decode(file_get_contents($fileName), true); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Bridge/League/SlugifyServiceProvider.php: -------------------------------------------------------------------------------- 1 | container->share(SlugifyInterface::class, function () { 20 | $options = []; 21 | if ($this->container->has('config.slugify.options')) { 22 | $options = $this->container->get('config.slugify.options'); 23 | } 24 | 25 | $provider = null; 26 | if ($this->container->has(RuleProviderInterface::class)) { 27 | /* @var RuleProviderInterface $provider */ 28 | $provider = $this->container->get(RuleProviderInterface::class); 29 | } 30 | 31 | return new Slugify( 32 | $options, 33 | $provider 34 | ); 35 | }); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /vendor/composer/autoload_real.php: -------------------------------------------------------------------------------- 1 | register(true); 35 | 36 | return $loader; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Renderer/PsrProvider.php: -------------------------------------------------------------------------------- 1 | defaultRenderer; 26 | } 27 | 28 | if (!$this->container->has($name)) { 29 | throw new \InvalidArgumentException(\sprintf('The renderer "%s" is not defined.', $name)); 30 | } 31 | 32 | return $this->container->get($name); 33 | } 34 | 35 | public function has(string $name): bool 36 | { 37 | return $this->container->has($name); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Bridge/Plum/SlugifyConverter.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Cocur\Slugify\Bridge\Plum; 13 | 14 | use Plum\Plum\Converter\ConverterInterface; 15 | use Cocur\Slugify\Slugify; 16 | use Cocur\Slugify\SlugifyInterface; 17 | 18 | /** 19 | * SlugifyConverter 20 | * 21 | * @package Cocur\Slugify\Bridge\Plum 22 | * @author Florian Eckerstorfer 23 | * @copyright 2015 Florian Eckerstorfer 24 | */ 25 | class SlugifyConverter implements ConverterInterface 26 | { 27 | /** @var Slugify */ 28 | private $slugify; 29 | 30 | /** 31 | * @param SlugifyInterface|null $slugify 32 | */ 33 | public function __construct(SlugifyInterface $slugify = null) 34 | { 35 | if ($slugify === null) { 36 | $slugify = new Slugify(); 37 | } 38 | $this->slugify = $slugify; 39 | } 40 | 41 | /** 42 | * @param string $item 43 | * 44 | * @return string 45 | */ 46 | public function convert($item): string 47 | { 48 | return $this->slugify->slugify($item); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Renderer/ArrayAccessProvider.php: -------------------------------------------------------------------------------- 1 | $registry 12 | * @param string $defaultRenderer The name of the renderer used by default 13 | * @param array $rendererIds The map between renderer names and registry keys 14 | */ 15 | public function __construct(private \ArrayAccess $registry, private string $defaultRenderer, private array $rendererIds) 16 | { 17 | } 18 | 19 | public function get(?string $name = null): RendererInterface 20 | { 21 | if (null === $name) { 22 | $name = $this->defaultRenderer; 23 | } 24 | 25 | if (!isset($this->rendererIds[$name]) || null === $this->registry[$this->rendererIds[$name]]) { 26 | throw new \InvalidArgumentException(\sprintf('The renderer "%s" is not defined.', $name)); 27 | } 28 | 29 | return $this->registry[$this->rendererIds[$name]]; 30 | } 31 | 32 | public function has(string $name): bool 33 | { 34 | return isset($this->rendererIds[$name]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Integration/Symfony/RoutingExtension.php: -------------------------------------------------------------------------------- 1 | generator->generate($options['route'], $params, $absolute); 24 | 25 | // adding the item route to the extras under the 'routes' key (for the Silex RouteVoter) 26 | $options['extras']['routes'][] = [ 27 | 'route' => $options['route'], 28 | 'parameters' => $params, 29 | ]; 30 | } 31 | 32 | return $options; 33 | } 34 | 35 | public function buildItem(ItemInterface $item, array $options): void 36 | { 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Provider/ArrayAccessProvider.php: -------------------------------------------------------------------------------- 1 | $registry 18 | * @param array $menuIds The map between menu identifiers and registry keys 19 | */ 20 | public function __construct(private \ArrayAccess $registry, private array $menuIds = []) 21 | { 22 | } 23 | 24 | public function get(string $name, array $options = []): ItemInterface 25 | { 26 | if (!isset($this->menuIds[$name])) { 27 | throw new \InvalidArgumentException(\sprintf('The menu "%s" is not defined.', $name)); 28 | } 29 | 30 | $menu = $this->registry[$this->menuIds[$name]]; 31 | 32 | if (\is_callable($menu)) { 33 | $menu = $menu($options, $this->registry); 34 | } 35 | 36 | return $menu; 37 | } 38 | 39 | public function has(string $name, array $options = []): bool 40 | { 41 | return isset($this->menuIds[$name]); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Bridge/Laravel/SlugifyServiceProvider.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Cocur\Slugify\Bridge\Laravel; 13 | 14 | use Cocur\Slugify\Slugify; 15 | use Illuminate\Support\ServiceProvider as LaravelServiceProvider; 16 | 17 | /** 18 | * SlugifyServiceProvider 19 | * 20 | * @package cocur/slugify 21 | * @subpackage bridge 22 | * @author Florian Eckerstorfer 23 | * @author Colin Viebrock 24 | * @copyright 2012-2014 Florian Eckerstorfer 25 | * @license http://www.opensource.org/licenses/MIT The MIT License 26 | */ 27 | class SlugifyServiceProvider extends LaravelServiceProvider 28 | { 29 | /** 30 | * Indicates if loading of the provider is deferred. 31 | * 32 | * @var bool 33 | */ 34 | protected $defer = true; 35 | 36 | /** 37 | * Register the service provider. 38 | * 39 | * @return void 40 | */ 41 | public function register(): void 42 | { 43 | $this->app->singleton('slugify', function () { 44 | return new Slugify(); 45 | }); 46 | } 47 | 48 | /** 49 | * Get the services provided by the provider. 50 | * 51 | * @return string[] 52 | */ 53 | public function provides(): array 54 | { 55 | return ['slugify']; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Provider/LazyProvider.php: -------------------------------------------------------------------------------- 1 | $builders 18 | */ 19 | public function __construct(private array $builders) 20 | { 21 | } 22 | 23 | public function get(string $name, array $options = []): ItemInterface 24 | { 25 | if (!isset($this->builders[$name])) { 26 | throw new \InvalidArgumentException(\sprintf('The menu "%s" is not defined.', $name)); 27 | } 28 | 29 | $builder = $this->builders[$name]; 30 | 31 | if (\is_array($builder) && isset($builder[0]) && $builder[0] instanceof \Closure) { 32 | $builder[0] = $builder[0](); 33 | } 34 | 35 | if (!\is_callable($builder)) { 36 | throw new \LogicException(\sprintf('Invalid menu builder for "%s". A callable or a factory for an object callable are expected.', $name)); 37 | } 38 | 39 | return $builder($options); 40 | } 41 | 42 | public function has(string $name, array $options = []): bool 43 | { 44 | return isset($this->builders[$name]); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Bridge/ZF2/Module.php: -------------------------------------------------------------------------------- 1 | > 23 | */ 24 | public function getServiceConfig(): array 25 | { 26 | return [ 27 | 'factories' => [ 28 | 'Cocur\Slugify\Slugify' => 'Cocur\Slugify\Bridge\ZF2\SlugifyService' 29 | ], 30 | 'aliases' => [ 31 | 'slugify' => 'Cocur\Slugify\Slugify' 32 | ] 33 | ]; 34 | } 35 | 36 | /** 37 | * Expected to return \Zend\ServiceManager\Config object or array to 38 | * seed such an object. 39 | * 40 | * @return array>|\Zend\ServiceManager\Config 41 | */ 42 | public function getViewHelperConfig(): array 43 | { 44 | return [ 45 | 'factories' => [ 46 | 'slugify' => 'Cocur\Slugify\Bridge\ZF2\SlugifyViewHelperFactory' 47 | ] 48 | ]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Bridge/Symfony/Configuration.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Cocur\Slugify\Bridge\Symfony; 13 | 14 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 15 | use Symfony\Component\Config\Definition\ConfigurationInterface; 16 | 17 | class Configuration implements ConfigurationInterface 18 | { 19 | /** 20 | * {@inheritdoc} 21 | */ 22 | public function getConfigTreeBuilder(): TreeBuilder 23 | { 24 | $treeBuilder = new TreeBuilder('cocur_slugify'); 25 | 26 | // Keep compatibility with symfony/config < 4.2 27 | if (\method_exists($treeBuilder, 'getRootNode')) { 28 | $rootNode = $treeBuilder->getRootNode(); 29 | } else { 30 | $rootNode = $treeBuilder->root('cocur_slugify'); 31 | } 32 | 33 | $rootNode 34 | ->children() 35 | ->booleanNode('lowercase')->end() 36 | ->booleanNode('lowercase_after_regexp')->end() 37 | ->booleanNode('trim')->end() 38 | ->booleanNode('strip_tags')->end() 39 | ->scalarNode('separator')->end() 40 | ->scalarNode('regexp')->end() 41 | ->arrayNode('rulesets')->prototype('scalar')->end() 42 | ->end(); 43 | 44 | return $treeBuilder; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /classes/shortcodes/AnchorShortcode.php: -------------------------------------------------------------------------------- 1 | shortcode->getRawHandlers()->add('anchor', function(ProcessedShortcode $sc) { 14 | 15 | $id = $this->cleanParam($sc->getParameter('id', $sc->getBbCode())); 16 | $tag = $this->cleanParam($sc->getParameter('tag')); 17 | $prefix = $this->cleanParam($sc->getParameter('prefix', PageTOCPlugin::configVar('anchors.slug_prefix'))); 18 | $class = $this->cleanParam($sc->getParameter('class', 'inline-anchor')); 19 | $aria = PageTOCPlugin::configVar('anchors.aria'); 20 | $content = $sc->getContent(); 21 | 22 | $slugger = new UniqueSlugify(); 23 | 24 | if (is_null($id)) { 25 | $id = $slugger->slugify(strip_tags($content)); 26 | } 27 | 28 | if (isset($prefix)) { 29 | $id = $prefix . $id; 30 | } 31 | 32 | if ($tag) { 33 | $output = "<$tag id=\"$id\" class=\"$class\">$content"; 34 | } else { 35 | $output = "$content"; 36 | } 37 | 38 | return $output; 39 | }); 40 | $this->shortcode->getRawHandlers()->addAlias('#', 'anchor'); 41 | } 42 | 43 | /** 44 | * @param $param 45 | * @return string 46 | */ 47 | protected function cleanParam($param) 48 | { 49 | return trim(html_entity_decode($param), '"'); 50 | } 51 | } -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Renderer/TwigRenderer.php: -------------------------------------------------------------------------------- 1 | $defaultOptions 13 | */ 14 | public function __construct( 15 | private Environment $environment, 16 | string $template, 17 | private MatcherInterface $matcher, 18 | private array $defaultOptions = [] 19 | ) { 20 | $this->defaultOptions = \array_merge([ 21 | 'depth' => null, 22 | 'matchingDepth' => null, 23 | 'currentAsLink' => true, 24 | 'currentClass' => 'current', 25 | 'ancestorClass' => 'current_ancestor', 26 | 'firstClass' => 'first', 27 | 'lastClass' => 'last', 28 | 'template' => $template, 29 | 'compressed' => false, 30 | 'allow_safe_labels' => false, 31 | 'clear_matcher' => true, 32 | 'leaf_class' => null, 33 | 'branch_class' => null, 34 | ], $defaultOptions); 35 | } 36 | 37 | public function render(ItemInterface $item, array $options = []): string 38 | { 39 | $options = \array_merge($this->defaultOptions, $options); 40 | 41 | $html = $this->environment->render($options['template'], ['item' => $item, 'options' => $options, 'matcher' => $this->matcher]); 42 | 43 | if ($options['clear_matcher']) { 44 | $this->matcher->clear(); 45 | } 46 | 47 | return $html; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /vendor/composer/installed.php: -------------------------------------------------------------------------------- 1 | array( 3 | 'name' => 'trilbymedia/page-toc', 4 | 'pretty_version' => 'dev-develop', 5 | 'version' => 'dev-develop', 6 | 'reference' => '4b5cc37caa17cbd68dc876fa2a6470a8d706ede6', 7 | 'type' => 'grav-plugin', 8 | 'install_path' => __DIR__ . '/../../', 9 | 'aliases' => array(), 10 | 'dev' => true, 11 | ), 12 | 'versions' => array( 13 | 'cocur/slugify' => array( 14 | 'pretty_version' => 'v4.6.0', 15 | 'version' => '4.6.0.0', 16 | 'reference' => '1d674022e9cbefa80b4f51aa3e2375b6e3c14fdb', 17 | 'type' => 'library', 18 | 'install_path' => __DIR__ . '/../cocur/slugify', 19 | 'aliases' => array(), 20 | 'dev_requirement' => false, 21 | ), 22 | 'knplabs/knp-menu' => array( 23 | 'pretty_version' => 'v3.5.0', 24 | 'version' => '3.5.0.0', 25 | 'reference' => 'c39403f7c427d1b72cc56f38df0a075b4b9191fe', 26 | 'type' => 'library', 27 | 'install_path' => __DIR__ . '/../knplabs/knp-menu', 28 | 'aliases' => array(), 29 | 'dev_requirement' => false, 30 | ), 31 | 'trilbymedia/page-toc' => array( 32 | 'pretty_version' => 'dev-develop', 33 | 'version' => 'dev-develop', 34 | 'reference' => '4b5cc37caa17cbd68dc876fa2a6470a8d706ede6', 35 | 'type' => 'grav-plugin', 36 | 'install_path' => __DIR__ . '/../../', 37 | 'aliases' => array(), 38 | 'dev_requirement' => false, 39 | ), 40 | ), 41 | ); 42 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Loader/ArrayLoader.php: -------------------------------------------------------------------------------- 1 | supports($data)) { 22 | throw new \InvalidArgumentException(\sprintf('Unsupported data. Expected an array but got %s', \get_debug_type($data))); 23 | } 24 | 25 | return $this->fromArray($data); 26 | } 27 | 28 | public function supports($data): bool 29 | { 30 | return \is_array($data); 31 | } 32 | 33 | /** 34 | * @param array $data 35 | * @param string|null $name (the name of the item, used only if there is no name in the data themselves) 36 | */ 37 | private function fromArray(array $data, ?string $name = null): ItemInterface 38 | { 39 | $name = $data['name'] ?? $name; 40 | 41 | if (isset($data['children'])) { 42 | $children = $data['children']; 43 | unset($data['children']); 44 | } else { 45 | $children = []; 46 | } 47 | 48 | $item = $this->factory->createItem($name, $data); 49 | 50 | foreach ($children as $childName => $child) { 51 | $item->addChild($this->fromArray($child, $childName)); 52 | } 53 | 54 | return $item; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "knplabs/knp-menu", 3 | "type": "library", 4 | "description": "An object oriented menu library", 5 | "keywords": ["menu", "tree"], 6 | "homepage": "https://knplabs.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "KnpLabs", 11 | "homepage": "https://knplabs.com" 12 | }, 13 | { 14 | "name": "Christophe Coevoet", 15 | "email": "stof@notk.org" 16 | }, 17 | { 18 | "name": "The Community", 19 | "homepage": "https://github.com/KnpLabs/KnpMenu/contributors" 20 | } 21 | ], 22 | "require": { 23 | "php": "^8.1" 24 | }, 25 | "conflict": { 26 | "twig/twig": "<1.42.3 || >=2,<2.9" 27 | }, 28 | "require-dev": { 29 | "phpstan/phpstan": "^1.10", 30 | "phpunit/phpunit": "^9.6", 31 | "psr/container": "^1.0 || ^2.0", 32 | "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0", 33 | "symfony/phpunit-bridge": "^7.0", 34 | "symfony/routing": "^5.4 || ^6.0 || ^7.0", 35 | "twig/twig": "^2.16 || ^3.0" 36 | }, 37 | "suggest": { 38 | "twig/twig": "for the TwigRenderer and the integration with your templates" 39 | }, 40 | "autoload": { 41 | "psr-4": { "Knp\\Menu\\": "src/Knp/Menu" } 42 | }, 43 | "autoload-dev": { 44 | "psr-4": { 45 | "Knp\\Menu\\Tests\\": "tests/Knp/Menu/Tests" 46 | } 47 | }, 48 | "config": { 49 | "sort-packages": true 50 | }, 51 | "extra": { 52 | "branch-alias": { 53 | "dev-master": "3.x-dev" 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Bridge/Nette/SlugifyExtension.php: -------------------------------------------------------------------------------- 1 | 14 | * @license http://www.opensource.org/licenses/MIT The MIT License 15 | */ 16 | class SlugifyExtension extends CompilerExtension 17 | { 18 | public function loadConfiguration(): void 19 | { 20 | $builder = $this->getContainerBuilder(); 21 | 22 | $builder->addDefinition($this->prefix('slugify')) 23 | ->setClass('Cocur\Slugify\SlugifyInterface') 24 | ->setFactory('Cocur\Slugify\Slugify'); 25 | 26 | $builder->addDefinition($this->prefix('helper')) 27 | ->setClass('Cocur\Slugify\Bridge\Latte\SlugifyHelper') 28 | ->setAutowired(false); 29 | } 30 | 31 | public function beforeCompile(): void 32 | { 33 | $builder = $this->getContainerBuilder(); 34 | 35 | $self = $this; 36 | $registerToLatte = function (ServiceDefinition $def) use ($self) { 37 | $def->addSetup('addFilter', ['slugify', [$self->prefix('@helper'), 'slugify']]); 38 | }; 39 | 40 | $latteFactory = $builder->getByType('Nette\Bridges\ApplicationLatte\ILatteFactory') ?: 'nette.latteFactory'; 41 | if ($builder->hasDefinition($latteFactory)) { 42 | $registerToLatte($builder->getDefinition($latteFactory)); 43 | } 44 | 45 | if ($builder->hasDefinition('nette.latte')) { 46 | $registerToLatte($builder->getDefinition('nette.latte')); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /vendor/composer/autoload_static.php: -------------------------------------------------------------------------------- 1 | 11 | array ( 12 | 'Knp\\Menu\\' => 9, 13 | ), 14 | 'G' => 15 | array ( 16 | 'Grav\\Plugin\\PageToc\\' => 20, 17 | ), 18 | 'C' => 19 | array ( 20 | 'Cocur\\Slugify\\' => 14, 21 | ), 22 | ); 23 | 24 | public static $prefixDirsPsr4 = array ( 25 | 'Knp\\Menu\\' => 26 | array ( 27 | 0 => __DIR__ . '/..' . '/knplabs/knp-menu/src/Knp/Menu', 28 | ), 29 | 'Grav\\Plugin\\PageToc\\' => 30 | array ( 31 | 0 => __DIR__ . '/../..' . '/classes', 32 | ), 33 | 'Cocur\\Slugify\\' => 34 | array ( 35 | 0 => __DIR__ . '/..' . '/cocur/slugify/src', 36 | ), 37 | ); 38 | 39 | public static $classMap = array ( 40 | 'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php', 41 | 'Grav\\Plugin\\PageTOCPlugin' => __DIR__ . '/../..' . '/page-toc.php', 42 | ); 43 | 44 | public static function getInitializer(ClassLoader $loader) 45 | { 46 | return \Closure::bind(function () use ($loader) { 47 | $loader->prefixLengthsPsr4 = ComposerStaticInit88b09b7fda3e99f2e22346b58205e375::$prefixLengthsPsr4; 48 | $loader->prefixDirsPsr4 = ComposerStaticInit88b09b7fda3e99f2e22346b58205e375::$prefixDirsPsr4; 49 | $loader->classMap = ComposerStaticInit88b09b7fda3e99f2e22346b58205e375::$classMap; 50 | 51 | }, null, ClassLoader::class); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/MenuFactory.php: -------------------------------------------------------------------------------- 1 | > 15 | */ 16 | private array $extensions = []; 17 | 18 | /** 19 | * @var ExtensionInterface[]|null 20 | */ 21 | private ?array $sorted = null; 22 | 23 | public function __construct() 24 | { 25 | $this->addExtension(new CoreExtension(), -10); 26 | } 27 | 28 | public function createItem(string $name, array $options = []): ItemInterface 29 | { 30 | foreach ($this->getExtensions() as $extension) { 31 | $options = $extension->buildOptions($options); 32 | } 33 | 34 | $item = new MenuItem($name, $this); 35 | 36 | foreach ($this->getExtensions() as $extension) { 37 | $extension->buildItem($item, $options); 38 | } 39 | 40 | return $item; 41 | } 42 | 43 | /** 44 | * Adds a factory extension 45 | */ 46 | public function addExtension(ExtensionInterface $extension, int $priority = 0): void 47 | { 48 | $this->extensions[$priority][] = $extension; 49 | $this->sorted = null; 50 | } 51 | 52 | /** 53 | * Sorts the internal list of extensions by priority. 54 | * 55 | * @return ExtensionInterface[] 56 | */ 57 | private function getExtensions(): array 58 | { 59 | if (null === $this->sorted) { 60 | \krsort($this->extensions); 61 | $this->sorted = !empty($this->extensions) ? \array_merge(...$this->extensions) : []; 62 | } 63 | 64 | return $this->sorted; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /classes/UniqueSlugify.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * Licensed under MIT, see LICENSE. 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Grav\Plugin\PageToc; 17 | 18 | use Cocur\Slugify\Slugify; 19 | use Cocur\Slugify\SlugifyInterface; 20 | 21 | /** 22 | * UniqueSluggify creates slugs from text without repeating the same slug twice per instance 23 | */ 24 | class UniqueSlugify implements SlugifyInterface 25 | { 26 | protected $slugify; 27 | protected $used; 28 | 29 | protected $options; 30 | 31 | /** 32 | * Constructor 33 | * 34 | * @param SlugifyInterface|null $slugify 35 | */ 36 | public function __construct() 37 | { 38 | $this->used = array(); 39 | $this->slugify = new Slugify(); 40 | } 41 | 42 | /** 43 | * Slugify 44 | * 45 | * @param string $text 46 | * @param array|null $options 47 | * @return string 48 | */ 49 | public function slugify($text, $options = null): string 50 | { 51 | $slugged = $this->slugify->slugify($text, $options); 52 | 53 | $maxlen = $options['maxlen'] ?? null; 54 | $prefix = $options['prefix'] ?? null; 55 | 56 | if (is_int($maxlen) && strlen($slugged) > $maxlen) { 57 | $slugged = substr($slugged, 0, $maxlen); 58 | } 59 | 60 | if (isset($prefix)) { 61 | $slugged = $prefix . $slugged; 62 | } 63 | 64 | $count = 1; 65 | $orig = $slugged; 66 | while (in_array($slugged, $this->used)) { 67 | $slugged = $orig . '-' . $count; 68 | $count++; 69 | } 70 | 71 | $this->used[] = $slugged; 72 | return $slugged; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /classes/OrderedListRenderer.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * Licensed under MIT, see LICENSE. 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Grav\Plugin\PageToc; 17 | 18 | use Knp\Menu\ItemInterface; 19 | use Knp\Menu\Renderer\ListRenderer; 20 | 21 | use function str_repeat; 22 | 23 | /** 24 | * Class OrderedListRenderer 25 | * 26 | * @package TOC 27 | */ 28 | class OrderedListRenderer extends ListRenderer 29 | { 30 | /** 31 | * @param ItemInterface $item 32 | * @param array $attributes 33 | * @param array $options 34 | * @return string 35 | */ 36 | protected function renderList(ItemInterface $item, array $attributes, array $options): string 37 | { 38 | if (!$item->hasChildren() || 0 === $options['depth'] || !$item->getDisplayChildren()) { 39 | return ''; 40 | } 41 | 42 | $html = $this->format( 43 | 'renderHtmlAttributes($attributes) . '>', 44 | 'ol', 45 | $item->getLevel(), 46 | $options 47 | ); 48 | 49 | $html .= $this->renderChildren($item, $options); 50 | $html .= $this->format('', 'ol', $item->getLevel(), $options); 51 | 52 | return $html; 53 | } 54 | 55 | /** 56 | * @param string $html 57 | * @param string $type 58 | * @param int $level 59 | * @param array $options 60 | * @return string 61 | */ 62 | protected function format(string $html, string $type, int $level, array $options): string 63 | { 64 | return $type === 'ol' 65 | ? str_repeat(' ', $level * 4) . $html . "\n" 66 | : parent::format($html, $type, $level, $options); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cocur/slugify", 3 | "type": "library", 4 | "description": "Converts a string into a slug.", 5 | "keywords": [ 6 | "slug", 7 | "slugify" 8 | ], 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Florian Eckerstorfer", 13 | "email": "florian@eckerstorfer.co", 14 | "homepage": "https://florian.ec" 15 | }, 16 | { 17 | "name": "Ivo Bathke", 18 | "email": "ivo.bathke@gmail.com" 19 | } 20 | ], 21 | "require": { 22 | "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0", 23 | "ext-mbstring": "*" 24 | }, 25 | "conflict": { 26 | "symfony/config": "<3.4 || >=4,<4.3", 27 | "symfony/dependency-injection": "<3.4 || >=4,<4.3", 28 | "symfony/http-kernel": "<3.4 || >=4,<4.3", 29 | "twig/twig": "<2.12.1" 30 | }, 31 | "require-dev": { 32 | "laravel/framework": "^5.0|^6.0|^7.0|^8.0", 33 | "latte/latte": "~2.2", 34 | "league/container": "^2.2.0", 35 | "mikey179/vfsstream": "~1.6.8", 36 | "mockery/mockery": "^1.3", 37 | "nette/di": "~2.4", 38 | "pimple/pimple": "~1.1", 39 | "plumphp/plum": "~0.1", 40 | "symfony/config": "^3.4 || ^4.3 || ^5.0 || ^6.0", 41 | "symfony/dependency-injection": "^3.4 || ^4.3 || ^5.0 || ^6.0", 42 | "symfony/http-kernel": "^3.4 || ^4.3 || ^5.0 || ^6.0", 43 | "symfony/phpunit-bridge": "^5.4 || ^6.0", 44 | "twig/twig": "^2.12.1 || ~3.0", 45 | "zendframework/zend-modulemanager": "~2.2", 46 | "zendframework/zend-servicemanager": "~2.2", 47 | "zendframework/zend-view": "~2.2" 48 | }, 49 | "autoload": { 50 | "psr-4": { 51 | "Cocur\\Slugify\\": "src" 52 | } 53 | }, 54 | "autoload-dev": { 55 | "psr-4": { 56 | "Cocur\\Slugify\\Tests\\": "tests" 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Bridge/Twig/SlugifyExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Cocur\Slugify\Bridge\Twig; 13 | 14 | use Cocur\Slugify\SlugifyInterface; 15 | use Twig\Extension\AbstractExtension; 16 | use Twig\TwigFilter; 17 | 18 | /** 19 | * SlugifyExtension 20 | * 21 | * @package cocur/slugify 22 | * @subpackage bridge 23 | * @author Florian Eckerstorfer 24 | * @copyright 2012-2015 Florian Eckerstorfer 25 | * @license http://www.opensource.org/licenses/MIT The MIT License 26 | */ 27 | class SlugifyExtension extends AbstractExtension 28 | { 29 | /** 30 | * @var SlugifyInterface 31 | */ 32 | private $slugify; 33 | 34 | /** 35 | * Constructor. 36 | * 37 | * @param SlugifyInterface $slugify 38 | * 39 | * @codeCoverageIgnore 40 | */ 41 | public function __construct(SlugifyInterface $slugify) 42 | { 43 | $this->slugify = $slugify; 44 | } 45 | 46 | /** 47 | * Returns the Twig functions of this extension. 48 | * 49 | * @return TwigFilter[] 50 | */ 51 | public function getFilters(): array 52 | { 53 | return [ 54 | new TwigFilter('slugify', [$this, 'slugifyFilter']), 55 | ]; 56 | } 57 | 58 | /** 59 | * Slugify filter. 60 | * 61 | * @param string $string 62 | * @param string|null $separator 63 | * 64 | * @return string 65 | */ 66 | public function slugifyFilter($string, $separator = null): string 67 | { 68 | return $this->slugify->slugify($string, $separator); 69 | } 70 | 71 | /** 72 | * get Name 73 | * 74 | * @return string 75 | */ 76 | public function getName(): string 77 | { 78 | return "SlugifyExtension"; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Matcher/Matcher.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | private \SplObjectStorage $cache; 17 | 18 | /** 19 | * @var iterable|VoterInterface[] 20 | */ 21 | private iterable $voters; 22 | 23 | /** 24 | * @param VoterInterface[]|iterable $voters 25 | */ 26 | public function __construct($voters = []) 27 | { 28 | $this->voters = $voters; 29 | $this->cache = new \SplObjectStorage(); 30 | } 31 | 32 | public function isCurrent(ItemInterface $item): bool 33 | { 34 | $current = $item->isCurrent(); 35 | if (null !== $current) { 36 | return $current; 37 | } 38 | 39 | if ($this->cache->contains($item)) { 40 | return $this->cache[$item]; 41 | } 42 | 43 | foreach ($this->voters as $voter) { 44 | $current = $voter->matchItem($item); 45 | if (null !== $current) { 46 | break; 47 | } 48 | } 49 | 50 | $current = (bool) $current; 51 | $this->cache[$item] = $current; 52 | 53 | return $current; 54 | } 55 | 56 | public function isAncestor(ItemInterface $item, ?int $depth = null): bool 57 | { 58 | if (0 === $depth) { 59 | return false; 60 | } 61 | 62 | $childDepth = null === $depth ? null : $depth - 1; 63 | foreach ($item->getChildren() as $child) { 64 | if ($this->isCurrent($child) || $this->isAncestor($child, $childDepth)) { 65 | return true; 66 | } 67 | } 68 | 69 | return false; 70 | } 71 | 72 | public function clear(): void 73 | { 74 | $this->cache = new \SplObjectStorage(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Factory/CoreExtension.php: -------------------------------------------------------------------------------- 1 | null, 17 | 'label' => null, 18 | 'attributes' => [], 19 | 'linkAttributes' => [], 20 | 'childrenAttributes' => [], 21 | 'labelAttributes' => [], 22 | 'extras' => [], 23 | 'current' => null, 24 | 'display' => true, 25 | 'displayChildren' => true, 26 | ], 27 | $options 28 | ); 29 | } 30 | 31 | public function buildItem(ItemInterface $item, array $options): void 32 | { 33 | $item 34 | ->setUri($options['uri']) 35 | ->setLabel($options['label']) 36 | ->setAttributes($options['attributes']) 37 | ->setLinkAttributes($options['linkAttributes']) 38 | ->setChildrenAttributes($options['childrenAttributes']) 39 | ->setLabelAttributes($options['labelAttributes']) 40 | ->setCurrent($options['current']) 41 | ->setDisplay($options['display']) 42 | ->setDisplayChildren($options['displayChildren']) 43 | ; 44 | 45 | $this->buildExtras($item, $options); 46 | } 47 | 48 | /** 49 | * Configures the newly created item's extras 50 | * Extras are processed one by one in order not to reset values set by other extensions 51 | * 52 | * @param array> $options 53 | */ 54 | private function buildExtras(ItemInterface $item, array $options): void 55 | { 56 | if (!empty($options['extras'])) { 57 | foreach ($options['extras'] as $key => $value) { 58 | $item->setExtra($key, $value); 59 | } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Bridge/Symfony/CocurSlugifyExtension.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Cocur\Slugify\Bridge\Symfony; 13 | 14 | use Cocur\Slugify\Bridge\Twig\SlugifyExtension; 15 | use Cocur\Slugify\Slugify; 16 | use Cocur\Slugify\SlugifyInterface; 17 | use Symfony\Component\DependencyInjection\ContainerBuilder; 18 | use Symfony\Component\DependencyInjection\Definition; 19 | use Symfony\Component\DependencyInjection\Reference; 20 | use Symfony\Component\DependencyInjection\Extension\Extension; 21 | 22 | /** 23 | * CocurSlugifyExtension 24 | * 25 | * @package cocur/slugify 26 | * @subpackage bridge 27 | * @author Florian Eckerstorfer 28 | * @copyright 2012-2014 Florian Eckerstorfer 29 | * @license http://www.opensource.org/licenses/MIT The MIT License 30 | */ 31 | class CocurSlugifyExtension extends Extension 32 | { 33 | /** 34 | * {@inheritDoc} 35 | * 36 | * @param mixed[] $configs 37 | * @param ContainerBuilder $container 38 | */ 39 | public function load(array $configs, ContainerBuilder $container): void 40 | { 41 | $configuration = new Configuration(); 42 | $config = $this->processConfiguration($configuration, $configs); 43 | 44 | if (empty($config['rulesets'])) { 45 | unset($config['rulesets']); 46 | } 47 | 48 | // Extract slugify arguments from config 49 | $slugifyArguments = array_intersect_key($config, array_flip(['lowercase', 'trim', 'strip_tags', 'separator', 'regexp', 'rulesets'])); 50 | 51 | $container->setDefinition('cocur_slugify', new Definition(Slugify::class, [$slugifyArguments])); 52 | $container 53 | ->setDefinition( 54 | 'cocur_slugify.twig.slugify', 55 | new Definition( 56 | SlugifyExtension::class, 57 | [new Reference('cocur_slugify')] 58 | ) 59 | ) 60 | ->addTag('twig.extension') 61 | ->setPublic(false); 62 | $container->setAlias('slugify', 'cocur_slugify'); 63 | $container->setAlias(SlugifyInterface::class, 'cocur_slugify'); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /classes/MarkupFixer.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * Licensed under MIT, see LICENSE. 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Grav\Plugin\PageToc; 17 | 18 | use DOMElement; 19 | use RuntimeException; 20 | use Cocur\Slugify\SlugifyInterface; 21 | 22 | /** 23 | * TOC Markup Fixer adds `id` attributes to all H1...H6 tags where they do not 24 | * already exist 25 | * 26 | * @author Casey McLaughlin 27 | */ 28 | class MarkupFixer 29 | { 30 | use HtmlHelper; 31 | 32 | /** 33 | * Fix markup 34 | * 35 | * @param string $markup 36 | * @param int $start 37 | * @param int $depth 38 | * @param array $options 39 | * @return string Markup with added IDs 40 | * @throws RuntimeException 41 | */ 42 | public function fix(string $markup, array $options = []): ?string 43 | { 44 | $start = (int) $options['start'] ?? 1; 45 | $depth = (int) $options['depth'] ?? 6; 46 | $domDocument = $this->getHTMLParser($markup); 47 | $slugger = new UniqueSlugify(); 48 | 49 | /** @var DOMElement $node */ 50 | foreach ($this->traverseHeaderTags($domDocument, $start, $depth) as $node) { 51 | if ($node->getAttribute('id')) { 52 | $slug = $node->getAttribute('id'); 53 | } else { 54 | $slug = $slugger->slugify($node->getAttribute('title') ?: $node->textContent, $options); 55 | } 56 | 57 | $node->setAttribute('id', $slug); 58 | 59 | if ($options['hclass']) { 60 | $class = $node->getAttribute('class'); 61 | $node->setAttribute('class', trim("$class {$options['hclass']}")); 62 | } 63 | 64 | if ($options['link']) { 65 | $link = $domDocument->createElement("a"); 66 | $class = isset($options['class']) ? " {$options['class']}" : ""; 67 | $link->setAttribute('href', "#$slug"); 68 | $link->setAttribute('class', "toc-anchor {$options['position']}$class"); 69 | $link->setAttribute('data-anchor-icon', $options['icon']); 70 | $link->setAttribute('aria-label', $options['aria']); 71 | if ($options['position'] == 'after') { 72 | $node->appendChild($link); 73 | } else { 74 | $node->insertBefore($link, $node->firstChild); 75 | } 76 | } 77 | } 78 | 79 | $markup = $domDocument->saveHTML($domDocument->getElementsByTagName('page-toc')->item(0)); 80 | return str_replace(['', ''], '', $markup); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Renderer/Renderer.php: -------------------------------------------------------------------------------- 1 | charset = $charset; 13 | } 14 | } 15 | 16 | /** 17 | * Renders a HTML attribute 18 | * 19 | * @param string|bool $value 20 | */ 21 | protected function renderHtmlAttribute(string $name, $value): string 22 | { 23 | if (true === $value) { 24 | return \sprintf('%s="%s"', $name, $this->escape($name)); 25 | } 26 | if (false === $value) { 27 | throw new \InvalidArgumentException('Value cannot be false.'); 28 | } 29 | 30 | return \sprintf('%s="%s"', $name, $this->escape($value)); 31 | } 32 | 33 | /** 34 | * Renders HTML attributes 35 | * 36 | * @param array $attributes 37 | */ 38 | protected function renderHtmlAttributes(array $attributes): string 39 | { 40 | return \implode('', \array_map([$this, 'htmlAttributesCallback'], \array_keys($attributes), \array_values($attributes))); 41 | } 42 | 43 | /** 44 | * Prepares an attribute key and value for HTML representation. 45 | * 46 | * It removes empty attributes. 47 | * 48 | * @param string $name The attribute name 49 | * @param string|bool|null $value The attribute value 50 | * 51 | * @return string the HTML representation of the HTML key attribute pair 52 | */ 53 | private function htmlAttributesCallback(string $name, $value): string 54 | { 55 | if (false === $value || null === $value) { 56 | return ''; 57 | } 58 | 59 | return ' '.$this->renderHtmlAttribute($name, $value); 60 | } 61 | 62 | /** 63 | * Escapes an HTML value 64 | */ 65 | protected function escape(string $value): string 66 | { 67 | return $this->fixDoubleEscape(\htmlspecialchars($value, \ENT_QUOTES | \ENT_SUBSTITUTE, $this->charset)); 68 | } 69 | 70 | /** 71 | * Fixes double escaped strings. 72 | * 73 | * @param string $escaped string to fix 74 | * 75 | * @return string A single escaped string 76 | */ 77 | protected function fixDoubleEscape(string $escaped): string 78 | { 79 | return (string) \preg_replace('/&([a-z]+|(#\d+)|(#x[\da-f]+));/i', '&$1;', $escaped); 80 | } 81 | 82 | /** 83 | * Get the HTML charset 84 | */ 85 | public function getCharset(): string 86 | { 87 | return $this->charset; 88 | } 89 | 90 | /** 91 | * Set the HTML charset 92 | */ 93 | public function setCharset(string $charset): void 94 | { 95 | $this->charset = $charset; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /classes/HtmlHelper.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * Licensed under MIT, see LICENSE. 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Grav\Plugin\PageToc; 17 | 18 | use ArrayIterator; 19 | use DOMDocument; 20 | use DomElement; 21 | use DomNode; 22 | use DOMXPath; 23 | 24 | /** 25 | * Trait that helps with HTML-related operations 26 | * 27 | * @package TOC 28 | */ 29 | trait HtmlHelper 30 | { 31 | protected function getHTMLParser($markup) 32 | { 33 | libxml_use_internal_errors(true); 34 | $domDocument = new \DOMDocument(); 35 | 36 | $html = "$markup"; 37 | $domDocument->loadHTML(mb_encode_numericentity($html, [0x80, 0x10FFFF, 0, ~0], 'UTF-8'), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); 38 | $domDocument->preserveWhiteSpace = true; 39 | return $domDocument; 40 | } 41 | 42 | /** 43 | * Convert a topLevel and depth to H1..H6 tags array 44 | * 45 | * @param int $topLevel 46 | * @param int $depth 47 | * @return array|string[] Array of header tags; ex: ['h1', 'h2', 'h3'] 48 | */ 49 | protected function determineHeaderTags(int $topLevel, int $depth): array 50 | { 51 | $desired = range((int) $topLevel, (int) $topLevel + ((int) $depth - 1)); 52 | $allowed = [1, 2, 3, 4, 5, 6]; 53 | 54 | return array_map(function ($val) { 55 | return 'h' . $val; 56 | }, array_intersect($desired, $allowed)); 57 | } 58 | 59 | 60 | 61 | /** 62 | * Traverse Header Tags in DOM Document 63 | * 64 | * @param DOMDocument $domDocument 65 | * @param int $topLevel 66 | * @param int $depth 67 | * @return ArrayIterator 68 | */ 69 | protected function traverseHeaderTags(DOMDocument $domDocument, int $topLevel, int $depth): ArrayIterator 70 | { 71 | $xQueryResults = new DOMXPath($domDocument); 72 | 73 | $xpathQuery = sprintf( 74 | "//*[%s]", 75 | implode(' or ', array_map(function ($v) { 76 | return sprintf('local-name() = "%s"', $v); 77 | }, $this->determineHeaderTags($topLevel, $depth))) 78 | ); 79 | 80 | $nodes = []; 81 | $xQueryResults = $xQueryResults->query($xpathQuery); 82 | 83 | if ($xQueryResults) { 84 | foreach ($xQueryResults as $node) { 85 | $nodes[] = $node; 86 | } 87 | return new ArrayIterator($nodes); 88 | } else { 89 | return new ArrayIterator([]); 90 | } 91 | } 92 | 93 | protected function filteredInnerHTML(DOMNode $element, array $allowedTags): string 94 | { 95 | $innerHTML = ""; 96 | $children = $element->childNodes; 97 | 98 | foreach ($children as $child) { 99 | $innerHTML .= $element->ownerDocument->saveHTML($child); 100 | } 101 | 102 | return strip_tags($innerHTML, $allowedTags); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Resources/views/knp_menu.html.twig: -------------------------------------------------------------------------------- 1 | {% extends 'knp_menu_base.html.twig' %} 2 | 3 | {% macro attributes(attributes) %} 4 | {% for name, value in attributes %} 5 | {%- if value is not none and value is not same as(false) -%} 6 | {{- ' %s="%s"'|format(name, value is same as(true) ? name|e : value|e)|raw -}} 7 | {%- endif -%} 8 | {%- endfor -%} 9 | {% endmacro %} 10 | 11 | {% block compressed_root %} 12 | {% apply spaceless %} 13 | {{ block('root') }} 14 | {% endapply %} 15 | {% endblock %} 16 | 17 | {% block root %} 18 | {% set listAttributes = item.childrenAttributes %} 19 | {{ block('list') -}} 20 | {% endblock %} 21 | 22 | {% block list %} 23 | {% if item.hasChildren and options.depth is not same as(0) and item.displayChildren %} 24 | {% import _self as knp_menu %} 25 | 26 | {{ block('children') }} 27 | 28 | {% endif %} 29 | {% endblock %} 30 | 31 | {% block children %} 32 | {# save current variables #} 33 | {% set currentOptions = options %} 34 | {% set currentItem = item %} 35 | {# update the depth for children #} 36 | {% if options.depth is not none %} 37 | {% set options = options|merge({'depth': currentOptions.depth - 1}) %} 38 | {% endif %} 39 | {# update the matchingDepth for children #} 40 | {% if options.matchingDepth is not none and options.matchingDepth > 0 %} 41 | {% set options = options|merge({'matchingDepth': currentOptions.matchingDepth - 1}) %} 42 | {% endif %} 43 | {% for item in currentItem.children %} 44 | {{ block('item') }} 45 | {% endfor %} 46 | {# restore current variables #} 47 | {% set item = currentItem %} 48 | {% set options = currentOptions %} 49 | {% endblock %} 50 | 51 | {% block item %} 52 | {% if item.displayed %} 53 | {# building the class of the item #} 54 | {%- set classes = item.attribute('class') is not empty ? [item.attribute('class')] : [] %} 55 | {%- if matcher.isCurrent(item) %} 56 | {%- set classes = classes|merge([options.currentClass]) %} 57 | {%- elseif matcher.isAncestor(item, options.matchingDepth) %} 58 | {%- set classes = classes|merge([options.ancestorClass]) %} 59 | {%- endif %} 60 | {%- if item.actsLikeFirst %} 61 | {%- set classes = classes|merge([options.firstClass]) %} 62 | {%- endif %} 63 | {%- if item.actsLikeLast %} 64 | {%- set classes = classes|merge([options.lastClass]) %} 65 | {%- endif %} 66 | 67 | {# Mark item as "leaf" (no children) or as "branch" (has children that are displayed) #} 68 | {% if item.hasChildren and options.depth is not same as(0) %} 69 | {% if options.branch_class is not empty and item.displayChildren %} 70 | {%- set classes = classes|merge([options.branch_class]) %} 71 | {% endif %} 72 | {% elseif options.leaf_class is not empty %} 73 | {%- set classes = classes|merge([options.leaf_class]) %} 74 | {%- endif %} 75 | 76 | {%- set attributes = item.attributes %} 77 | {%- if classes is not empty %} 78 | {%- set attributes = attributes|merge({'class': classes|join(' ')}) %} 79 | {%- endif %} 80 | {# displaying the item #} 81 | {% import _self as knp_menu %} 82 | 83 | {%- if item.uri is not empty and (not matcher.isCurrent(item) or options.currentAsLink) %} 84 | {{ block('linkElement') }} 85 | {%- else %} 86 | {{ block('spanElement') }} 87 | {%- endif %} 88 | {# render the list of children#} 89 | {%- set childrenClasses = item.childrenAttribute('class') is not empty ? [item.childrenAttribute('class')] : [] %} 90 | {%- set childrenClasses = childrenClasses|merge(['menu_level_' ~ item.level]) %} 91 | {%- set listAttributes = item.childrenAttributes|merge({'class': childrenClasses|join(' ') }) %} 92 | {{ block('list') }} 93 | 94 | {% endif %} 95 | {% endblock %} 96 | 97 | {% block linkElement %}{% import _self as knp_menu %}{{ block('label') }}{% endblock %} 98 | 99 | {% block spanElement %}{% import _self as knp_menu %}{{ block('label') }}{% endblock %} 100 | 101 | {% block label %}{% if options.allow_safe_labels and item.getExtra('safe_label', false) %}{{ item.label|raw }}{% else %}{{ item.label }}{% endif %}{% endblock %} 102 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Matcher/Voter/RouteVoter.php: -------------------------------------------------------------------------------- 1 | requestStack, 'getMainRequest')) { 21 | $request = $this->requestStack->getMainRequest(); // symfony 5.3+ 22 | } else { 23 | $request = $this->requestStack->getMasterRequest(); 24 | } 25 | 26 | if (null === $request) { 27 | return null; 28 | } 29 | 30 | $route = $request->attributes->get('_route'); 31 | if (null === $route) { 32 | return null; 33 | } 34 | 35 | $routes = (array) $item->getExtra('routes', []); 36 | 37 | foreach ($routes as $testedRoute) { 38 | if (\is_string($testedRoute)) { 39 | $testedRoute = ['route' => $testedRoute]; 40 | } 41 | 42 | if (!\is_array($testedRoute)) { 43 | throw new \InvalidArgumentException('Routes extra items must be strings or arrays.'); 44 | } 45 | 46 | if ($this->isMatchingRoute($request, $testedRoute)) { 47 | return true; 48 | } 49 | } 50 | 51 | return null; 52 | } 53 | 54 | /** 55 | * @phpstan-param array{route?: string|null, pattern?: string|null, parameters?: array, query_parameters?: array} $testedRoute 56 | */ 57 | private function isMatchingRoute(Request $request, array $testedRoute): bool 58 | { 59 | $route = $request->attributes->get('_route'); 60 | 61 | if (isset($testedRoute['route'])) { 62 | if ($route !== $testedRoute['route']) { 63 | return false; 64 | } 65 | } elseif (!empty($testedRoute['pattern'])) { 66 | if (!\preg_match($testedRoute['pattern'], $route)) { 67 | return false; 68 | } 69 | } else { 70 | throw new \InvalidArgumentException('Routes extra items must have a "route" or "pattern" key.'); 71 | } 72 | 73 | return $this->isMatchingParameters($request, $testedRoute) && $this->isMatchingQueryParameters($request, $testedRoute); 74 | } 75 | 76 | /** 77 | * @phpstan-param array{route?: string|null, pattern?: string|null, parameters?: array, query_parameters?: array} $testedRoute 78 | */ 79 | private function isMatchingParameters(Request $request, array $testedRoute): bool 80 | { 81 | if (!isset($testedRoute['parameters'])) { 82 | return true; 83 | } 84 | 85 | $routeParameters = $request->attributes->get('_route_params', []); 86 | 87 | foreach ($testedRoute['parameters'] as $name => $value) { 88 | // cast both to string so that we handle integer and other non-string parameters, but don't stumble on 0 == 'abc'. 89 | if (!isset($routeParameters[$name]) || (string) $routeParameters[$name] !== (string) $value) { 90 | return false; 91 | } 92 | } 93 | 94 | return true; 95 | } 96 | 97 | /** 98 | * @phpstan-param array{route?: string|null, pattern?: string|null, parameters?: array, query_parameters?: array} $testedRoute 99 | */ 100 | private function isMatchingQueryParameters(Request $request, array $testedRoute): bool 101 | { 102 | if (!isset($testedRoute['query_parameters'])) { 103 | return true; 104 | } 105 | 106 | $routeQueryParameters = $request->query->all(); 107 | 108 | foreach ($testedRoute['query_parameters'] as $name => $value) { 109 | // cast both to string so that we handle integer and other non-string parameters, but don't stumble on 0 == 'abc'. 110 | if (!isset($routeQueryParameters[$name]) || \is_array($routeQueryParameters[$name]) || (string) $routeQueryParameters[$name] !== (string) $value) { 111 | return false; 112 | } 113 | } 114 | 115 | return true; 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v4.0.0-beta.1 2 | ## 12/10/2024 3 | 4 | 1. [](#improved) 5 | * Updates for PHP 8.2 support, now requires Grav 1.8+ 6 | 7 | # v3.2.4 8 | ## 05/16/2024 9 | 10 | 1. [](#improved) 11 | * Updated Spanish translation [#40](https://github.com/trilbymedia/grav-plugin-page-toc/pulls/40) 12 | 1. [](#bugfix) 13 | * Reverted to use `mb_encode_nuericentity()` instead of `htmlspecialchars` / `iconv` approach to fix breaking UTF-8 characters 14 | 15 | # v3.2.3 16 | ## 05/06/2024 17 | 18 | 1. [](#new) 19 | * Added french translation [#28](https://github.com/trilbymedia/grav-plugin-page-toc/pulls/28) 20 | * Added option to whitelist HTML tags in TOC [#36](https://github.com/trilbymedia/grav-plugin-page-toc/pulls/36) 21 | * Added option to set user templates in which anchors are generated [#37](https://github.com/trilbymedia/grav-plugin-page-toc/pulls/37) 22 | 1. [](#bugfix) 23 | * Revert Twig macro to use `_self` as it was breaking the recursion. Deprecated message remains but can't be helped. [#38](https://github.com/trilbymedia/grav-plugin-page-toc/issues/38) 24 | 25 | # v3.2.2 26 | ## 05/10/2023 27 | 28 | 1. [](#bugfix) 29 | * Use `mb_encode_nuericentity()` instead of `htmlspecialchars` / `iconv` approach to fix breaking UTF-8 characters 30 | * Fix a deprecated message in the Twig macro 31 | 32 | # v3.2.1 33 | ## 05/08/2023 34 | 35 | 1. [](#improved) 36 | * Fixed a "Deprecated: mb_convert_encoding()" error 37 | 38 | # v3.2.0 39 | ## 02/23/2022 40 | 41 | 1. [](#new) 42 | * Support for HTML or Shortcode based headers with custom `id` attributes to specify an anchor 43 | * Added German translation 44 | 45 | # v3.1.3 46 | ## 01/03/2022 47 | 48 | 1. [](#new) 49 | * Require Grav `v1.7.26` to make use of built in `Plugin::inheritedConfigOption()` 50 | * NOTE: `page-toc v3.1.2` was released prior to `Grav v1.7.26` and has been removed 51 | 2. [](#improved) 52 | * Don't force inclusion of `` or `` tags to reduce chance of invalid HTML 53 | * Improved `README.md` 54 | 55 | # v3.1.1 56 | ## 12/16/2021 57 | 58 | 1. [](#bugfix) 59 | * Fixed some blueprint errors that caused errors on save 60 | * Force `start` and `depth` to be integers [#17](https://github.com/trilbymedia/grav-plugin-page-toc/issues/17) 61 | 62 | # v3.1.0 63 | ## 12/09/2021 64 | 65 | 1. [](#new) 66 | * **NEW** Added option to automatically copying to clipboard an anchor URL when clicking on it 67 | 68 | # v3.0.0 69 | ## 12/03/2021 70 | 71 | 1. [](#new) 72 | * **NEW** Support built-in `anchors` with customization of icon/classes/css etc. 73 | * **NEW** `[anchor]` shortcode for creating manual anchors for easy linking to page content 74 | * Moved the vendor-based TOC functionality in-plugin to provide more flexibility and additional features 75 | * Added several more Twig functions for increased flexibility 76 | * Ability to limit the length of a fragment link 77 | * Ability to set a custom prefix for anchor links 78 | * Added `languages.yaml` file for text translations 79 | 2. [](#improved) 80 | * Independent control over the levels of anchors that should be built and the TOC displayed 81 | * `page-toc:` page-level configuration can be set in parent pages and trickles down to child pages 82 | * Removed dependency on HTML5 library and use the faster PHP `DOMDocument` class 83 | * Translated text for the "Table of Contents" in the `page-toc.html.twig` template 84 | 85 | # v2.0.0 86 | ## 11/24/2021 87 | 88 | 1. [](#new) 89 | * Added new `components/page-toc.html.twig` that can be extended and the HTML output modified 90 | * Updated core TOC library to latest `3.0.2` version 91 | * Requires PHP `7.3.6` 92 | * Requires Grav `1.7+` 93 | * Added Shortcode-like in-page syntax support. e.g. `[toc]` 94 | 95 | # v1.1.2 96 | ## 06/01/2021 97 | 98 | 1. [](#new) 99 | * Added page-toc blueprints under "Advanced" tab for admin 100 | 1. [](#improved) 101 | * Updated to latest `knplabs/knp-menu` library 102 | 1. [](#bugfix) 103 | * Added `|raw` filter to twig output in README.md 104 | 105 | # v1.1.1 106 | ## 12/02/2020 107 | 108 | 1. [](#improved) 109 | * Updated to latest `masterminds/html5` and `knplabs/knp-menu` libraries 110 | 111 | # v1.1.0 112 | ## 04/01/2019 113 | 114 | 1. [](#improved) 115 | * Updated to latest `caseyamcl/toc` library 116 | 1. [](#bugfix) 117 | * Fixes relative levels [#6](https://github.com/trilbymedia/grav-plugin-page-toc/pull/9) 118 | * Fixes incorrect reference to `end` when it should be `depth` [#7](https://github.com/trilbymedia/grav-plugin-page-toc/pull/7) 119 | 120 | # v1.0.1 121 | ## 03/19/2017 122 | 123 | 1. [](#improved) 124 | * Fixed issue with `end` not being valid, should be `depth`. Updated README 125 | 126 | # v1.0.0 127 | ## 08/01/2017 128 | 129 | 1. [](#new) 130 | * ChangeLog started... 131 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Twig/MenuExtension.php: -------------------------------------------------------------------------------- 1 | 21 | */ 22 | public function getFunctions(): array 23 | { 24 | return [ 25 | new TwigFunction('knp_menu_get', [$this, 'get']), 26 | new TwigFunction('knp_menu_render', [$this, 'render'], ['is_safe' => ['html']]), 27 | new TwigFunction('knp_menu_get_breadcrumbs_array', [$this, 'getBreadcrumbsArray']), 28 | new TwigFunction('knp_menu_get_current_item', [$this, 'getCurrentItem']), 29 | ]; 30 | } 31 | 32 | /** 33 | * @return array 34 | */ 35 | public function getFilters(): array 36 | { 37 | return [ 38 | new TwigFilter('knp_menu_as_string', [$this, 'pathAsString']), 39 | ]; 40 | } 41 | 42 | /** 43 | * @return array 44 | */ 45 | public function getTests(): array 46 | { 47 | return [ 48 | new TwigTest('knp_menu_current', [$this, 'isCurrent']), 49 | new TwigTest('knp_menu_ancestor', [$this, 'isAncestor']), 50 | ]; 51 | } 52 | 53 | /** 54 | * Retrieves an item following a path in the tree. 55 | * 56 | * @param ItemInterface|string $menu 57 | * @param array $path 58 | * @param array $options 59 | */ 60 | public function get($menu, array $path = [], array $options = []): ItemInterface 61 | { 62 | return $this->helper->get($menu, $path, $options); 63 | } 64 | 65 | /** 66 | * Renders a menu with the specified renderer. 67 | * 68 | * @param ItemInterface|string|array $menu 69 | * @param array $options 70 | */ 71 | public function render($menu, array $options = [], ?string $renderer = null): string 72 | { 73 | return $this->helper->render($menu, $options, $renderer); 74 | } 75 | 76 | /** 77 | * Returns an array ready to be used for breadcrumbs. 78 | * 79 | * @param ItemInterface|string|array $menu 80 | * @param string|array|null $subItem 81 | * 82 | * @phpstan-param string|ItemInterface|array|\Traversable $subItem 83 | * 84 | * @return array> 85 | * @phpstan-return list 86 | */ 87 | public function getBreadcrumbsArray($menu, $subItem = null): array 88 | { 89 | return $this->helper->getBreadcrumbsArray($menu, $subItem); 90 | } 91 | 92 | /** 93 | * Returns the current item of a menu. 94 | * 95 | * @param ItemInterface|string $menu 96 | */ 97 | public function getCurrentItem($menu): ItemInterface 98 | { 99 | $rootItem = $this->get($menu); 100 | 101 | $currentItem = $this->helper->getCurrentItem($rootItem); 102 | 103 | if (null === $currentItem) { 104 | $currentItem = $rootItem; 105 | } 106 | 107 | return $currentItem; 108 | } 109 | 110 | /** 111 | * A string representation of this menu item 112 | * 113 | * e.g. Top Level > Second Level > This menu 114 | */ 115 | public function pathAsString(ItemInterface $menu, string $separator = ' > '): string 116 | { 117 | if (null === $this->menuManipulator) { 118 | throw new \BadMethodCallException('The menu manipulator must be set to get the breadcrumbs array'); 119 | } 120 | 121 | return $this->menuManipulator->getPathAsString($menu, $separator); 122 | } 123 | 124 | /** 125 | * Checks whether an item is current. 126 | */ 127 | public function isCurrent(ItemInterface $item): bool 128 | { 129 | if (null === $this->matcher) { 130 | throw new \BadMethodCallException('The matcher must be set to get the breadcrumbs array'); 131 | } 132 | 133 | return $this->matcher->isCurrent($item); 134 | } 135 | 136 | /** 137 | * Checks whether an item is the ancestor of a current item. 138 | * 139 | * @param int|null $depth The max depth to look for the item 140 | */ 141 | public function isAncestor(ItemInterface $item, ?int $depth = null): bool 142 | { 143 | if (null === $this->matcher) { 144 | throw new \BadMethodCallException('The matcher must be set to get the breadcrumbs array'); 145 | } 146 | 147 | return $this->matcher->isAncestor($item, $depth); 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /blueprints.yaml: -------------------------------------------------------------------------------- 1 | name: Page Toc 2 | type: plugin 3 | slug: page-toc 4 | version: 4.0.0-beta.1 5 | testing: true 6 | description: Generate a table of contents and anchors from a page 7 | icon: list 8 | author: 9 | name: Trilby Media, LLC 10 | email: hello@trilby.media 11 | url: http://trilby.media 12 | homepage: http://trilby.media 13 | keywords: grav, plugin, toc, anchors 14 | bugs: https://github.com/trilbymedia/grav-plugin-page-toc/issues 15 | docs: https://github.com/trilbymedia/grav-plugin-page-toc/blob/develop/README.md 16 | license: MIT 17 | 18 | dependencies: 19 | - { name: grav, version: '>=1.8.0-beta.1' } 20 | 21 | form: 22 | validation: strict 23 | fields: 24 | enabled: 25 | type: toggle 26 | label: PLUGIN_ADMIN.PLUGIN_STATUS 27 | highlight: 1 28 | default: 1 29 | options: 30 | 1: PLUGIN_ADMIN.ENABLED 31 | 0: PLUGIN_ADMIN.DISABLED 32 | validate: 33 | type: bool 34 | include_css: 35 | type: toggle 36 | label: PLUGIN_PAGE_TOC.INCLUDE_CSS 37 | highlight: 1 38 | default: 1 39 | options: 40 | 1: PLUGIN_ADMIN.ENABLED 41 | 0: PLUGIN_ADMIN.DISABLED 42 | validate: 43 | type: bool 44 | active: 45 | type: toggle 46 | label: PLUGIN_PAGE_TOC.ACTIVE_BY_DEFAULT 47 | highlight: 1 48 | default: 1 49 | options: 50 | 1: PLUGIN_ADMIN.ENABLED 51 | 0: PLUGIN_ADMIN.DISABLED 52 | validate: 53 | type: bool 54 | templates: 55 | type: selectize 56 | label: PLUGIN_PAGE_TOC.ACTIVE_FOR_TEMPLATES 57 | help: PLUGIN_PAGE_TOC.ACTIVE_FOR_TEMPLATES_HELP 58 | validate: 59 | type: commalist 60 | 61 | toc_section: 62 | type: section 63 | title: PLUGIN_PAGE_TOC.TOC_SECTION 64 | underline: true 65 | 66 | fields: 67 | start: 68 | type: select 69 | label: PLUGIN_PAGE_TOC.START_TOC_HEADERS 70 | help: PLUGIN_PAGE_TOC.START_TOC_HEADERS_HELP 71 | size: x-small 72 | classes: fancy 73 | options: 74 | 1: H1 75 | 2: H2 76 | 3: H3 77 | 4: H4 78 | 5: H5 79 | 6: H6 80 | validate: 81 | type: number 82 | depth: 83 | type: range 84 | label: PLUGIN_PAGE_TOC.DEPTH_TOC_HEADERS 85 | help: PLUGIN_PAGE_TOC.DEPTH_TOC_HEADERS_HELP 86 | classes: fancy 87 | validate: 88 | min: 1 89 | max: 6 90 | hclass: 91 | type: text 92 | label: PLUGIN_PAGE_TOC.HEADER_CSS_CLASSES 93 | help: PLUGIN_PAGE_TOC.HEADER_CSS_CLASSES_HELP 94 | tags: 95 | type: selectize 96 | label: PLUGIN_PAGE_TOC.ALLOWED_HTML_TAGS 97 | help: PLUGIN_PAGE_TOC.ALLOWED_HTML_TAGS_HELP 98 | validate: 99 | type: commalist 100 | 101 | anchors_section: 102 | type: section 103 | title: PLUGIN_PAGE_TOC.ANCHORS_SECTION 104 | underline: true 105 | 106 | fields: 107 | anchors.start: 108 | type: select 109 | label: PLUGIN_PAGE_TOC.START_ANCHOR_HEADERS 110 | size: x-small 111 | classes: fancy 112 | options: 113 | 1: H1 114 | 2: H2 115 | 3: H3 116 | 4: H4 117 | 5: H5 118 | 6: H6 119 | validate: 120 | type: number 121 | anchors.depth: 122 | type: range 123 | label: PLUGIN_PAGE_TOC.DEPTH_ANCHOR_HEADERS 124 | help: PLUGIN_PAGE_TOC.DEPTH_ANCHOR_HEADERS_HELP 125 | classes: fancy 126 | validate: 127 | min: 1 128 | max: 6 129 | anchors.link: 130 | type: toggle 131 | label: PLUGIN_PAGE_TOC.LINK_ANCHOR_HEADERS 132 | highlight: 1 133 | default: 1 134 | options: 135 | 1: Enabled 136 | 0: Disabled 137 | validate: 138 | type: bool 139 | anchors.aria: 140 | type: text 141 | label: PLUGIN_PAGE_TOC.ARIA_LABEL 142 | default: Anchor 143 | anchors.class: 144 | type: text 145 | label: PLUGIN_PAGE_TOC.ANCHORS_CLASS 146 | help: PLUGIN_PAGE_TOC.ANCHORS_CLASS_HELP 147 | anchors.icon: 148 | type: text 149 | label: PLUGIN_PAGE_TOC.ANCHORS_ICON 150 | help: PLUGIN_PAGE_TOC.ANCHORS_ICON_HELP 151 | default: '#' 152 | size: x-small 153 | anchors.position: 154 | type: select 155 | label: PLUGIN_PAGE_TOC.ANCHORS_POSITION 156 | help: PLUGIN_PAGE_TOC.ANCHORS_POSITION_HELP 157 | size: small 158 | default: after 159 | options: 160 | before: PLUGIN_PAGE_TOC.BEFORE_TEXT 161 | after: PLUGIN_PAGE_TOC.AFTER_TEXT 162 | anchors.copy_to_clipboard: 163 | type: toggle 164 | label: PLUGIN_PAGE_TOC.COPY_TO_CLIPBOARD 165 | help: PLUGIN_PAGE_TOC.COPY_TO_CLIPBOARD_HELP 166 | highlight: 1 167 | default: 1 168 | options: 169 | 1: Enabled 170 | 0: Disabled 171 | validate: 172 | type: bool 173 | anchors.slug_maxlen: 174 | type: number 175 | label: PLUGIN_PAGE_TOC.SLUG_MAXLEN 176 | help: PLUGIN_PAGE_TOC.SLUG_MAXLEN_HELP 177 | size: x-small 178 | default: 25 179 | append: 'chars' 180 | anchors.slug_prefix: 181 | type: text 182 | label: PLUGIN_PAGE_TOC.SLUG_PREFIX 183 | help: PLUGIN_PAGE_TOC.SLUG_PREFIX_HELP 184 | -------------------------------------------------------------------------------- /vendor/cocur/slugify/src/Slugify.php: -------------------------------------------------------------------------------- 1 | 7 | * 8 | * For the full copyright and license information, please view the LICENSE 9 | * file that was distributed with this source code. 10 | */ 11 | 12 | namespace Cocur\Slugify; 13 | 14 | use Cocur\Slugify\RuleProvider\DefaultRuleProvider; 15 | use Cocur\Slugify\RuleProvider\RuleProviderInterface; 16 | 17 | /** 18 | * Slugify 19 | * 20 | * @package Cocur\Slugify 21 | * @author Florian Eckerstorfer 22 | * @author Ivo Bathke 23 | * @author Marchenko Alexandr 24 | * @copyright 2012-2015 Florian Eckerstorfer 25 | * @license http://www.opensource.org/licenses/MIT The MIT License 26 | */ 27 | class Slugify implements SlugifyInterface 28 | { 29 | public const LOWERCASE_NUMBERS_DASHES = '/[^A-Za-z0-9]+/'; 30 | 31 | /** 32 | * @var array 33 | */ 34 | protected array $rules = []; 35 | 36 | /** 37 | * @var RuleProviderInterface 38 | */ 39 | protected RuleProviderInterface $provider; 40 | 41 | /** 42 | * @var array 43 | */ 44 | protected array $options = [ 45 | 'regexp' => self::LOWERCASE_NUMBERS_DASHES, 46 | 'separator' => '-', 47 | 'lowercase' => true, 48 | 'lowercase_after_regexp' => false, 49 | 'trim' => true, 50 | 'strip_tags' => false, 51 | 'rulesets' => [ 52 | 'default', 53 | // Languages are preferred if they appear later, list is ordered by number of 54 | // websites in that language 55 | // https://en.wikipedia.org/wiki/Languages_used_on_the_Internet#Content_languages_for_websites 56 | 'yiddish', 57 | 'armenian', 58 | 'azerbaijani', 59 | 'burmese', 60 | 'hindi', 61 | 'georgian', 62 | 'norwegian', 63 | 'vietnamese', 64 | 'ukrainian', 65 | 'latvian', 66 | 'finnish', 67 | 'greek', 68 | 'czech', 69 | 'arabic', 70 | 'slovak', 71 | 'turkish', 72 | 'polish', 73 | 'german', 74 | 'russian', 75 | 'romanian' 76 | ], 77 | ]; 78 | 79 | /** 80 | * @param array $options 81 | * @param RuleProviderInterface $provider 82 | */ 83 | public function __construct(array $options = [], ?RuleProviderInterface $provider = null) 84 | { 85 | $this->options = array_merge($this->options, $options); 86 | $this->provider = $provider ? $provider : new DefaultRuleProvider(); 87 | 88 | foreach ($this->options['rulesets'] as $ruleSet) { 89 | $this->activateRuleSet($ruleSet); 90 | } 91 | } 92 | 93 | /** 94 | * Returns the slug-version of the string. 95 | * 96 | * @param string $string String to slugify 97 | * @param string|array|null $options Options 98 | * 99 | * @return string Slugified version of the string 100 | */ 101 | public function slugify(string $string, array|string|null $options = null): string 102 | { 103 | // BC: the second argument used to be the separator 104 | if (is_string($options)) { 105 | $separator = $options; 106 | $options = []; 107 | $options['separator'] = $separator; 108 | } 109 | 110 | $options = array_merge($this->options, (array) $options); 111 | 112 | // Add a custom ruleset without touching the default rules 113 | if (isset($options['ruleset'])) { 114 | $rules = array_merge($this->rules, $this->provider->getRules($options['ruleset'])); 115 | } else { 116 | $rules = $this->rules; 117 | } 118 | 119 | $string = ($options['strip_tags']) 120 | ? strip_tags($string) 121 | : $string; 122 | 123 | $string = strtr($string, $rules); 124 | unset($rules); 125 | 126 | if ($options['lowercase'] && !$options['lowercase_after_regexp']) { 127 | $string = mb_strtolower($string); 128 | } 129 | 130 | $string = preg_replace($options['regexp'], $options['separator'], $string); 131 | 132 | if ($options['lowercase'] && $options['lowercase_after_regexp']) { 133 | $string = mb_strtolower($string); 134 | } 135 | 136 | return ($options['trim']) 137 | ? trim($string, $options['separator']) 138 | : $string; 139 | } 140 | 141 | /** 142 | * Adds a custom rule to Slugify. 143 | * 144 | * @param string $character Character 145 | * @param string $replacement Replacement character 146 | * 147 | * @return Slugify 148 | */ 149 | public function addRule($character, $replacement): self 150 | { 151 | $this->rules[$character] = $replacement; 152 | 153 | return $this; 154 | } 155 | 156 | /** 157 | * Adds multiple rules to Slugify. 158 | * 159 | * @param array $rules 160 | * 161 | * @return Slugify 162 | */ 163 | public function addRules(array $rules): self 164 | { 165 | foreach ($rules as $character => $replacement) { 166 | $this->addRule($character, $replacement); 167 | } 168 | 169 | return $this; 170 | } 171 | 172 | /** 173 | * @param string $ruleSet 174 | * 175 | * @return Slugify 176 | */ 177 | public function activateRuleSet($ruleSet): self 178 | { 179 | return $this->addRules($this->provider->getRules($ruleSet)); 180 | } 181 | 182 | /** 183 | * Static method to create new instance of {@see Slugify}. 184 | * 185 | * @param array $options 186 | * 187 | * @return Slugify 188 | */ 189 | public static function create(array $options = []): self 190 | { 191 | return new static($options); 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /classes/TocGenerator.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * Licensed under MIT, see LICENSE. 12 | */ 13 | 14 | declare(strict_types=1); 15 | 16 | namespace Grav\Plugin\PageToc; 17 | 18 | use Knp\Menu\ItemInterface; 19 | use Knp\Menu\Matcher\Matcher; 20 | use Knp\Menu\MenuFactory; 21 | use Knp\Menu\MenuItem; 22 | use Knp\Menu\Renderer\ListRenderer; 23 | use Knp\Menu\Renderer\RendererInterface; 24 | 25 | /** 26 | * Table Of Contents Generator generates TOCs from HTML Markup 27 | * 28 | * @author Casey McLaughlin 29 | */ 30 | class TocGenerator 31 | { 32 | use HtmlHelper; 33 | 34 | private const DEFAULT_NAME = 'TOC'; 35 | 36 | /** 37 | * @var MenuFactory 38 | */ 39 | private $menuFactory; 40 | 41 | /** 42 | * Constructor 43 | */ 44 | public function __construct() 45 | { 46 | $this->menuFactory = new MenuFactory(); 47 | } 48 | 49 | /** 50 | * Get Menu 51 | * 52 | * Returns a KNP Menu object, which can be traversed or rendered 53 | * 54 | * @param string $markup Content to get items fro $this->getItems($markup, $topLevel, $depth)m 55 | * @param int $topLevel Top Header (1 through 6) 56 | * @param int $depth Depth (1 through 6) 57 | * @return ItemInterface KNP Menu 58 | */ 59 | public function getMenu(string $markup, int $topLevel = 1, int $depth = 6, array $allowedTags = []): ItemInterface 60 | { 61 | $menu = $this->menuFactory->createItem(static::DEFAULT_NAME); 62 | 63 | if (trim($markup) == '') { 64 | return $menu; 65 | } 66 | 67 | $tagsToMatch = $this->determineHeaderTags($topLevel, $depth); 68 | $lastElem = $menu; 69 | 70 | $domDocument = $this->getHTMLParser($markup); 71 | 72 | foreach ($this->traverseHeaderTags($domDocument, $topLevel, $depth) as $i => $node) { 73 | if (! $node->hasAttribute('id')) { 74 | continue; 75 | } 76 | 77 | $tagName = $node->tagName; 78 | $level = array_search(strtolower($tagName), $tagsToMatch) + 1; 79 | 80 | /** @var MenuItem $parent */ 81 | if ($level == 1) { 82 | $parent = $menu; 83 | } elseif ($level == $lastElem->getLevel()) { 84 | $parent = $lastElem->getParent(); 85 | } elseif ($level > $lastElem->getLevel()) { 86 | $parent = $lastElem; 87 | for ($i = $lastElem->getLevel(); $i < ($level - 1); $i++) { 88 | $parent = $parent->addChild(''); 89 | } 90 | } else { //if ($level < $lastElem->getLevel()) 91 | $parent = $lastElem->getParent(); 92 | while ($parent->getLevel() > $level - 1) { 93 | $parent = $parent->getParent(); 94 | } 95 | } 96 | 97 | $lastElem = $parent->addChild( 98 | $node->getAttribute('id'), 99 | [ 100 | 'label' => $node->getAttribute('title') ?: 101 | ($allowedTags ? $this->filteredInnerHTML($node, $allowedTags) : $node->textContent) , 102 | 'uri' => '#' . $node->getAttribute('id') 103 | ] 104 | ); 105 | } 106 | 107 | return $this->trimMenu($menu); 108 | } 109 | 110 | /** 111 | * Trim empty items from the menu 112 | * 113 | * @param ItemInterface $menuItem 114 | * @return ItemInterface 115 | */ 116 | protected function trimMenu(ItemInterface $menuItem): ItemInterface 117 | { 118 | // if any of these circumstances are true, then just bail and return the menu item 119 | if ( 120 | count($menuItem->getChildren()) === 0 121 | or count($menuItem->getChildren()) > 1 122 | or ! empty($menuItem->getFirstChild()->getLabel()) 123 | ) { 124 | return $menuItem; 125 | } 126 | 127 | // otherwise, find the first level where there is actual content and use that. 128 | while (count($menuItem->getChildren()) == 1 && empty($menuItem->getFirstChild()->getLabel())) { 129 | $menuItem = $menuItem->getFirstChild(); 130 | } 131 | 132 | return $menuItem; 133 | } 134 | 135 | /** 136 | * Get HTML menu in unordered list form 137 | * 138 | * @param string $markup Content to get items from 139 | * @param int $topLevel Top Header (1 through 6) 140 | * @param int $depth Depth (1 through 6) 141 | * @param RendererInterface|null $renderer 142 | * @param bool $ordered 143 | * @return string HTML
  • items 144 | */ 145 | public function getHtmlMenu( 146 | string $markup, 147 | int $topLevel = 1, 148 | int $depth = 6, 149 | ?RendererInterface $renderer = null, 150 | bool $ordered = false 151 | ): string { 152 | if (! $renderer) { 153 | $options = ['currentClass' => 'active', 'ancestorClass' => 'active_ancestor']; 154 | $renderer = $ordered 155 | ? new OrderedListRenderer(new Matcher(), $options) 156 | : new ListRenderer(new Matcher(), $options); 157 | } 158 | 159 | $menu = $this->getMenu($markup, $topLevel, $depth); 160 | return $renderer->render($menu); 161 | } 162 | 163 | /** 164 | * Get HTML menu in ordered list form 165 | * 166 | * @param string $markup Content to get items from 167 | * @param int $topLevel Top Header (1 through 6) 168 | * @param int $depth Depth (1 through 6) 169 | * @param RendererInterface|null $renderer 170 | * @return string HTML
  • items 171 | */ 172 | public function getOrderedHtmlMenu( 173 | string $markup, 174 | int $topLevel = 1, 175 | int $depth = 6, 176 | ?RendererInterface $renderer = null 177 | ): string { 178 | return $this->getHtmlMenu($markup, $topLevel, $depth, $renderer, true); 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /vendor/composer/installed.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | { 4 | "name": "cocur/slugify", 5 | "version": "v4.6.0", 6 | "version_normalized": "4.6.0.0", 7 | "source": { 8 | "type": "git", 9 | "url": "https://github.com/cocur/slugify.git", 10 | "reference": "1d674022e9cbefa80b4f51aa3e2375b6e3c14fdb" 11 | }, 12 | "dist": { 13 | "type": "zip", 14 | "url": "https://api.github.com/repos/cocur/slugify/zipball/1d674022e9cbefa80b4f51aa3e2375b6e3c14fdb", 15 | "reference": "1d674022e9cbefa80b4f51aa3e2375b6e3c14fdb", 16 | "shasum": "" 17 | }, 18 | "require": { 19 | "ext-mbstring": "*", 20 | "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" 21 | }, 22 | "conflict": { 23 | "symfony/config": "<3.4 || >=4,<4.3", 24 | "symfony/dependency-injection": "<3.4 || >=4,<4.3", 25 | "symfony/http-kernel": "<3.4 || >=4,<4.3", 26 | "twig/twig": "<2.12.1" 27 | }, 28 | "require-dev": { 29 | "laravel/framework": "^5.0|^6.0|^7.0|^8.0", 30 | "latte/latte": "~2.2", 31 | "league/container": "^2.2.0", 32 | "mikey179/vfsstream": "~1.6.8", 33 | "mockery/mockery": "^1.3", 34 | "nette/di": "~2.4", 35 | "pimple/pimple": "~1.1", 36 | "plumphp/plum": "~0.1", 37 | "symfony/config": "^3.4 || ^4.3 || ^5.0 || ^6.0", 38 | "symfony/dependency-injection": "^3.4 || ^4.3 || ^5.0 || ^6.0", 39 | "symfony/http-kernel": "^3.4 || ^4.3 || ^5.0 || ^6.0", 40 | "symfony/phpunit-bridge": "^5.4 || ^6.0", 41 | "twig/twig": "^2.12.1 || ~3.0", 42 | "zendframework/zend-modulemanager": "~2.2", 43 | "zendframework/zend-servicemanager": "~2.2", 44 | "zendframework/zend-view": "~2.2" 45 | }, 46 | "time": "2024-09-10T14:09:25+00:00", 47 | "type": "library", 48 | "installation-source": "dist", 49 | "autoload": { 50 | "psr-4": { 51 | "Cocur\\Slugify\\": "src" 52 | } 53 | }, 54 | "notification-url": "https://packagist.org/downloads/", 55 | "license": [ 56 | "MIT" 57 | ], 58 | "authors": [ 59 | { 60 | "name": "Florian Eckerstorfer", 61 | "email": "florian@eckerstorfer.co", 62 | "homepage": "https://florian.ec" 63 | }, 64 | { 65 | "name": "Ivo Bathke", 66 | "email": "ivo.bathke@gmail.com" 67 | } 68 | ], 69 | "description": "Converts a string into a slug.", 70 | "keywords": [ 71 | "slug", 72 | "slugify" 73 | ], 74 | "support": { 75 | "issues": "https://github.com/cocur/slugify/issues", 76 | "source": "https://github.com/cocur/slugify/tree/v4.6.0" 77 | }, 78 | "install-path": "../cocur/slugify" 79 | }, 80 | { 81 | "name": "knplabs/knp-menu", 82 | "version": "v3.5.0", 83 | "version_normalized": "3.5.0.0", 84 | "source": { 85 | "type": "git", 86 | "url": "https://github.com/KnpLabs/KnpMenu.git", 87 | "reference": "c39403f7c427d1b72cc56f38df0a075b4b9191fe" 88 | }, 89 | "dist": { 90 | "type": "zip", 91 | "url": "https://api.github.com/repos/KnpLabs/KnpMenu/zipball/c39403f7c427d1b72cc56f38df0a075b4b9191fe", 92 | "reference": "c39403f7c427d1b72cc56f38df0a075b4b9191fe", 93 | "shasum": "" 94 | }, 95 | "require": { 96 | "php": "^8.1" 97 | }, 98 | "conflict": { 99 | "twig/twig": "<1.42.3 || >=2,<2.9" 100 | }, 101 | "require-dev": { 102 | "phpstan/phpstan": "^1.10", 103 | "phpunit/phpunit": "^9.6", 104 | "psr/container": "^1.0 || ^2.0", 105 | "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0", 106 | "symfony/phpunit-bridge": "^7.0", 107 | "symfony/routing": "^5.4 || ^6.0 || ^7.0", 108 | "twig/twig": "^2.16 || ^3.0" 109 | }, 110 | "suggest": { 111 | "twig/twig": "for the TwigRenderer and the integration with your templates" 112 | }, 113 | "time": "2024-03-23T15:35:09+00:00", 114 | "type": "library", 115 | "extra": { 116 | "branch-alias": { 117 | "dev-master": "3.x-dev" 118 | } 119 | }, 120 | "installation-source": "source", 121 | "autoload": { 122 | "psr-4": { 123 | "Knp\\Menu\\": "src/Knp/Menu" 124 | } 125 | }, 126 | "notification-url": "https://packagist.org/downloads/", 127 | "license": [ 128 | "MIT" 129 | ], 130 | "authors": [ 131 | { 132 | "name": "KnpLabs", 133 | "homepage": "https://knplabs.com" 134 | }, 135 | { 136 | "name": "Christophe Coevoet", 137 | "email": "stof@notk.org" 138 | }, 139 | { 140 | "name": "The Community", 141 | "homepage": "https://github.com/KnpLabs/KnpMenu/contributors" 142 | } 143 | ], 144 | "description": "An object oriented menu library", 145 | "homepage": "https://knplabs.com", 146 | "keywords": [ 147 | "menu", 148 | "tree" 149 | ], 150 | "support": { 151 | "issues": "https://github.com/KnpLabs/KnpMenu/issues", 152 | "source": "https://github.com/KnpLabs/KnpMenu/tree/v3.5.0" 153 | }, 154 | "install-path": "../knplabs/knp-menu" 155 | } 156 | ], 157 | "dev": true, 158 | "dev-package-names": [] 159 | } 160 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Twig/Helper.php: -------------------------------------------------------------------------------- 1 | $path 29 | * @param array $options 30 | * 31 | * @throws \BadMethodCallException when there is no menu provider and the menu is given by name 32 | * @throws \LogicException 33 | * @throws \InvalidArgumentException when the path is invalid 34 | */ 35 | public function get($menu, array $path = [], array $options = []): ItemInterface 36 | { 37 | if (!$menu instanceof ItemInterface) { 38 | if (null === $this->menuProvider) { 39 | throw new \BadMethodCallException('A menu provider must be set to retrieve a menu'); 40 | } 41 | 42 | $menuName = $menu; 43 | $menu = $this->menuProvider->get($menuName, $options); 44 | 45 | if (!$menu instanceof ItemInterface) { 46 | throw new \LogicException(\sprintf('The menu "%s" exists, but is not a valid menu item object. Check where you created the menu to be sure it returns an ItemInterface object.', $menuName)); 47 | } 48 | } 49 | 50 | foreach ($path as $child) { 51 | $menu = $menu->getChild($child); 52 | if (null === $menu) { 53 | throw new \InvalidArgumentException(\sprintf('The menu has no child named "%s"', $child)); 54 | } 55 | } 56 | 57 | return $menu; 58 | } 59 | 60 | /** 61 | * Renders a menu with the specified renderer. 62 | * 63 | * If the argument is an array, it will follow the path in the tree to 64 | * get the needed item. The first element of the array is the whole menu. 65 | * If the menu is a string instead of an ItemInterface, the provider 66 | * will be used. 67 | * 68 | * @param ItemInterface|string|array $menu 69 | * @param array $options 70 | * 71 | * @throws \InvalidArgumentException 72 | */ 73 | public function render($menu, array $options = [], ?string $renderer = null): string 74 | { 75 | $menu = $this->castMenu($menu); 76 | 77 | return $this->rendererProvider->get($renderer)->render($menu, $options); 78 | } 79 | 80 | /** 81 | * Renders an array ready to be used for breadcrumbs. 82 | * 83 | * Each element in the array will be an array with 3 keys: 84 | * - `label` containing the label of the item 85 | * - `url` containing the url of the item (may be `null`) 86 | * - `item` containing the original item (may be `null` for the extra items) 87 | * 88 | * The subItem can be one of the following forms 89 | * * 'subItem' 90 | * * ItemInterface object 91 | * * ['subItem' => '@homepage'] 92 | * * ['subItem1', 'subItem2'] 93 | * * [['label' => 'subItem1', 'url' => '@homepage'], ['label' => 'subItem2']] 94 | * 95 | * @param mixed $menu 96 | * @param mixed $subItem A string or array to append onto the end of the array 97 | * 98 | * @phpstan-param string|ItemInterface|array|\Traversable $subItem 99 | * 100 | * @return array> 101 | * 102 | * @phpstan-return list 103 | */ 104 | public function getBreadcrumbsArray($menu, $subItem = null): array 105 | { 106 | if (null === $this->menuManipulator) { 107 | throw new \BadMethodCallException('The menu manipulator must be set to get the breadcrumbs array'); 108 | } 109 | 110 | $menu = $this->castMenu($menu); 111 | 112 | return $this->menuManipulator->getBreadcrumbsArray($menu, $subItem); 113 | } 114 | 115 | /** 116 | * Returns the current item of a menu. 117 | * 118 | * @param ItemInterface|string|array $menu 119 | */ 120 | public function getCurrentItem($menu): ?ItemInterface 121 | { 122 | $menu = $this->castMenu($menu); 123 | 124 | return $this->retrieveCurrentItem($menu); 125 | } 126 | 127 | /** 128 | * @param ItemInterface|string|array $menu 129 | */ 130 | private function castMenu($menu): ItemInterface 131 | { 132 | if (!$menu instanceof ItemInterface) { 133 | $path = []; 134 | if (\is_array($menu)) { 135 | if (empty($menu)) { 136 | throw new \InvalidArgumentException('The array cannot be empty'); 137 | } 138 | $path = $menu; 139 | $menu = \array_shift($path); 140 | } 141 | 142 | return $this->get($menu, $path); 143 | } 144 | 145 | return $menu; 146 | } 147 | 148 | private function retrieveCurrentItem(ItemInterface $item): ?ItemInterface 149 | { 150 | if (null === $this->matcher) { 151 | throw new \BadMethodCallException('The matcher must be set to get the current item of a menu'); 152 | } 153 | 154 | if ($this->matcher->isCurrent($item)) { 155 | return $item; 156 | } 157 | 158 | if ($this->matcher->isAncestor($item)) { 159 | foreach ($item->getChildren() as $child) { 160 | $currentItem = $this->retrieveCurrentItem($child); 161 | 162 | if (null !== $currentItem) { 163 | return $currentItem; 164 | } 165 | } 166 | } 167 | 168 | return null; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /blueprints/page-toc.yaml: -------------------------------------------------------------------------------- 1 | form: 2 | fields: 3 | tabs: 4 | fields: 5 | advanced: 6 | type: tab 7 | 8 | fields: 9 | header.page-toc-section: 10 | type: section 11 | title: Page Table-of-Contents 12 | underline: true 13 | 14 | fields: 15 | header.page-toc.active: 16 | toggleable: true 17 | type: toggle 18 | label: PLUGIN_PAGE_TOC.ACTIVE_BY_DEFAULT_PAGE 19 | help: PLUGIN_PAGE_TOC.ACTIVE_BY_DEFAULT_PAGE_HELP 20 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'active'] 21 | options: 22 | 1: PLUGIN_ADMIN.YES 23 | 0: PLUGIN_ADMIN.NO 24 | validate: 25 | type: bool 26 | 27 | header.page-toc.start: 28 | toggleable: true 29 | type: select 30 | size: x-small 31 | label: PLUGIN_PAGE_TOC.START_TOC_HEADERS 32 | help: PLUGIN_PAGE_TOC.START_TOC_HEADERS_HELP 33 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'start'] 34 | options: 35 | 1: H1 36 | 2: H2 37 | 3: H3 38 | 4: H4 39 | 5: H5 40 | 6: H6 41 | validate: 42 | type: number 43 | header.page-toc.depth: 44 | toggleable: true 45 | type: range 46 | label: PLUGIN_PAGE_TOC.DEPTH_TOC_HEADERS 47 | help: PLUGIN_PAGE_TOC.DEPTH_TOC_HEADERS_HELP 48 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'depth'] 49 | validate: 50 | min: 1 51 | max: 6 52 | header.page-toc.hclass: 53 | toggleable: true 54 | type: text 55 | label: PLUGIN_PAGE_TOC.HEADER_CSS_CLASSES 56 | help: PLUGIN_PAGE_TOC.HEADER_CSS_CLASSES_HELP 57 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'hclass'] 58 | header.page-toc.tags: 59 | toggleable: true 60 | type: selectize 61 | label: PLUGIN_PAGE_TOC.ALLOWED_HTML_TAGS 62 | help: PLUGIN_PAGE_TOC.ALLOWED_HTML_TAGS_HELP 63 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'tags'] 64 | validate: 65 | type: commalist 66 | 67 | header.page-toc-anchors-section: 68 | type: section 69 | title: PLUGIN_PAGE_TOC.PAGE_ANCHORS_SECTION 70 | underline: true 71 | 72 | fields: 73 | header.page-toc.anchors.start: 74 | toggleable: true 75 | type: select 76 | label: PLUGIN_PAGE_TOC.START_ANCHOR_HEADERS 77 | size: x-small 78 | classes: fancy 79 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'anchors.start'] 80 | options: 81 | 1: H1 82 | 2: H2 83 | 3: H3 84 | 4: H4 85 | 5: H5 86 | 6: H6 87 | validate: 88 | type: number 89 | header.page-toc.anchors.depth: 90 | toggleable: true 91 | type: range 92 | label: PLUGIN_PAGE_TOC.DEPTH_ANCHOR_HEADERS 93 | classes: fancy 94 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'anchors.depth'] 95 | validate: 96 | min: 1 97 | max: 6 98 | header.page-toc.anchors.link: 99 | toggleable: true 100 | type: toggle 101 | label: PLUGIN_PAGE_TOC.LINK_ANCHOR_HEADERS 102 | highlight: 1 103 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'anchors.link'] 104 | options: 105 | 1: Enabled 106 | 0: Disabled 107 | validate: 108 | type: bool 109 | header.page-toc.anchors.aria: 110 | toggleable: true 111 | type: text 112 | label: PLUGIN_PAGE_TOC.ARIA_LABEL 113 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'anchors.aria'] 114 | header.page-toc.anchors.class: 115 | toggleable: true 116 | type: text 117 | label: PLUGIN_PAGE_TOC.ANCHORS_CLASS 118 | help: PLUGIN_PAGE_TOC.ANCHORS_CLASS_HELP 119 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'anchors.class'] 120 | header.page-toc.anchors.icon: 121 | toggleable: true 122 | type: text 123 | label: PLUGIN_PAGE_TOC.ANCHORS_ICON 124 | help: PLUGIN_PAGE_TOC.ANCHORS_ICON_HELP 125 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'anchors.icon'] 126 | size: x-small 127 | header.page-toc.anchors.position: 128 | toggleable: true 129 | type: select 130 | label: PLUGIN_PAGE_TOC.ANCHORS_POSITION 131 | help: PLUGIN_PAGE_TOC.ANCHORS_POSITION_HELP 132 | size: small 133 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'anchors.position'] 134 | options: 135 | before: PLUGIN_PAGE_TOC.BEFORE_TEXT 136 | after: PLUGIN_PAGE_TOC.AFTER_TEXT 137 | header.page-toc.anchors.slug_maxlen: 138 | toggleable: true 139 | type: number 140 | label: PLUGIN_PAGE_TOC.SLUG_MAXLEN 141 | help: PLUGIN_PAGE_TOC.SLUG_MAXLEN_HELP 142 | size: x-small 143 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'anchors.slug_maxlen'] 144 | append: 'chars' 145 | header.page-toc.anchors.slug_prefix: 146 | toggleable: true 147 | type: text 148 | label: PLUGIN_PAGE_TOC.SLUG_PREFIX 149 | help: PLUGIN_PAGE_TOC.SLUG_PREFIX_HELP 150 | data-default@: ['\Grav\Plugin\PageTOCPlugin::configVar', 'anchors.slug_prefix'] -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "031fb9270a55c32698ff626f5d6223ed", 8 | "packages": [ 9 | { 10 | "name": "cocur/slugify", 11 | "version": "v4.6.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/cocur/slugify.git", 15 | "reference": "1d674022e9cbefa80b4f51aa3e2375b6e3c14fdb" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/cocur/slugify/zipball/1d674022e9cbefa80b4f51aa3e2375b6e3c14fdb", 20 | "reference": "1d674022e9cbefa80b4f51aa3e2375b6e3c14fdb", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "ext-mbstring": "*", 25 | "php": "~8.0.0 || ~8.1.0 || ~8.2.0 || ~8.3.0 || ~8.4.0" 26 | }, 27 | "conflict": { 28 | "symfony/config": "<3.4 || >=4,<4.3", 29 | "symfony/dependency-injection": "<3.4 || >=4,<4.3", 30 | "symfony/http-kernel": "<3.4 || >=4,<4.3", 31 | "twig/twig": "<2.12.1" 32 | }, 33 | "require-dev": { 34 | "laravel/framework": "^5.0|^6.0|^7.0|^8.0", 35 | "latte/latte": "~2.2", 36 | "league/container": "^2.2.0", 37 | "mikey179/vfsstream": "~1.6.8", 38 | "mockery/mockery": "^1.3", 39 | "nette/di": "~2.4", 40 | "pimple/pimple": "~1.1", 41 | "plumphp/plum": "~0.1", 42 | "symfony/config": "^3.4 || ^4.3 || ^5.0 || ^6.0", 43 | "symfony/dependency-injection": "^3.4 || ^4.3 || ^5.0 || ^6.0", 44 | "symfony/http-kernel": "^3.4 || ^4.3 || ^5.0 || ^6.0", 45 | "symfony/phpunit-bridge": "^5.4 || ^6.0", 46 | "twig/twig": "^2.12.1 || ~3.0", 47 | "zendframework/zend-modulemanager": "~2.2", 48 | "zendframework/zend-servicemanager": "~2.2", 49 | "zendframework/zend-view": "~2.2" 50 | }, 51 | "type": "library", 52 | "autoload": { 53 | "psr-4": { 54 | "Cocur\\Slugify\\": "src" 55 | } 56 | }, 57 | "notification-url": "https://packagist.org/downloads/", 58 | "license": [ 59 | "MIT" 60 | ], 61 | "authors": [ 62 | { 63 | "name": "Florian Eckerstorfer", 64 | "email": "florian@eckerstorfer.co", 65 | "homepage": "https://florian.ec" 66 | }, 67 | { 68 | "name": "Ivo Bathke", 69 | "email": "ivo.bathke@gmail.com" 70 | } 71 | ], 72 | "description": "Converts a string into a slug.", 73 | "keywords": [ 74 | "slug", 75 | "slugify" 76 | ], 77 | "support": { 78 | "issues": "https://github.com/cocur/slugify/issues", 79 | "source": "https://github.com/cocur/slugify/tree/v4.6.0" 80 | }, 81 | "time": "2024-09-10T14:09:25+00:00" 82 | }, 83 | { 84 | "name": "knplabs/knp-menu", 85 | "version": "v3.5.0", 86 | "source": { 87 | "type": "git", 88 | "url": "https://github.com/KnpLabs/KnpMenu.git", 89 | "reference": "c39403f7c427d1b72cc56f38df0a075b4b9191fe" 90 | }, 91 | "dist": { 92 | "type": "zip", 93 | "url": "https://api.github.com/repos/KnpLabs/KnpMenu/zipball/c39403f7c427d1b72cc56f38df0a075b4b9191fe", 94 | "reference": "c39403f7c427d1b72cc56f38df0a075b4b9191fe", 95 | "shasum": "" 96 | }, 97 | "require": { 98 | "php": "^8.1" 99 | }, 100 | "conflict": { 101 | "twig/twig": "<1.42.3 || >=2,<2.9" 102 | }, 103 | "require-dev": { 104 | "phpstan/phpstan": "^1.10", 105 | "phpunit/phpunit": "^9.6", 106 | "psr/container": "^1.0 || ^2.0", 107 | "symfony/http-foundation": "^5.4 || ^6.0 || ^7.0", 108 | "symfony/phpunit-bridge": "^7.0", 109 | "symfony/routing": "^5.4 || ^6.0 || ^7.0", 110 | "twig/twig": "^2.16 || ^3.0" 111 | }, 112 | "suggest": { 113 | "twig/twig": "for the TwigRenderer and the integration with your templates" 114 | }, 115 | "type": "library", 116 | "extra": { 117 | "branch-alias": { 118 | "dev-master": "3.x-dev" 119 | } 120 | }, 121 | "autoload": { 122 | "psr-4": { 123 | "Knp\\Menu\\": "src/Knp/Menu" 124 | } 125 | }, 126 | "notification-url": "https://packagist.org/downloads/", 127 | "license": [ 128 | "MIT" 129 | ], 130 | "authors": [ 131 | { 132 | "name": "KnpLabs", 133 | "homepage": "https://knplabs.com" 134 | }, 135 | { 136 | "name": "Christophe Coevoet", 137 | "email": "stof@notk.org" 138 | }, 139 | { 140 | "name": "The Community", 141 | "homepage": "https://github.com/KnpLabs/KnpMenu/contributors" 142 | } 143 | ], 144 | "description": "An object oriented menu library", 145 | "homepage": "https://knplabs.com", 146 | "keywords": [ 147 | "menu", 148 | "tree" 149 | ], 150 | "support": { 151 | "issues": "https://github.com/KnpLabs/KnpMenu/issues", 152 | "source": "https://github.com/KnpLabs/KnpMenu/tree/v3.5.0" 153 | }, 154 | "time": "2024-03-23T15:35:09+00:00" 155 | } 156 | ], 157 | "packages-dev": [], 158 | "aliases": [], 159 | "minimum-stability": "stable", 160 | "stability-flags": {}, 161 | "prefer-stable": false, 162 | "prefer-lowest": false, 163 | "platform": { 164 | "php": ">=8.0" 165 | }, 166 | "platform-dev": {}, 167 | "platform-overrides": { 168 | "php": "8.2.0" 169 | }, 170 | "plugin-api-version": "2.6.0" 171 | } 172 | -------------------------------------------------------------------------------- /languages.yaml: -------------------------------------------------------------------------------- 1 | ru: 2 | PLUGIN_PAGE_TOC: 3 | TABLE_OF_CONTENTS: Содержание 4 | de: 5 | PLUGIN_PAGE_TOC: 6 | TABLE_OF_CONTENTS: Inhaltsverzeichnis 7 | en: 8 | PLUGIN_PAGE_TOC: 9 | TABLE_OF_CONTENTS: Table of Contents 10 | INCLUDE_CSS: 'Include CSS' 11 | ACTIVE_BY_DEFAULT: 'Anchors generated by default' 12 | ACTIVE_BY_DEFAULT_PAGE: 'Anchors generated for this page' 13 | ACTIVE_BY_DEFAULT_PAGE_HELP: 'If the default setting is disabled, you can enable on this page, or vice versa' 14 | ACTIVE_FOR_TEMPLATES: 'Anchors generated for these templates' 15 | ACTIVE_FOR_TEMPLATES_HELP: 'Only relevant if default (site-wide) anchor generation is disabled' 16 | TOC_SECTION: 'Table of Contents Configuration' 17 | START_TOC_HEADERS: 'Start TOC headers' 18 | START_TOC_HEADERS_HELP: 'The Header level to start the TOC from' 19 | DEPTH_TOC_HEADERS: 'Depth of TOC headers' 20 | DEPTH_TOC_HEADERS_HELP: 'The number of headers levels from the ''start'' to include in the TOC' 21 | HEADER_CSS_CLASSES: 'Header CSS classes' 22 | HEADER_CSS_CLASSES_HELP: 'Any custom classes to add to the header tags when IDs for slugs are added' 23 | ALLOWED_HTML_TAGS: 'Whitelisted HTML tags' 24 | ALLOWED_HTML_TAGS_HELP: 'These tags if present in the headers will be preserved in the TOC' 25 | ANCHORS_SECTION: 'Anchors Configuration' 26 | START_ANCHOR_HEADERS: 'Start Anchor headers' 27 | DEPTH_ANCHOR_HEADERS: 'Depth of Anchor headers' 28 | DEPTH_ANCHOR_HEADERS_HELP: 'The number of header levels from the ''start'' to be displayed' 29 | LINK_ANCHOR_HEADERS: 'Link Anchors' 30 | ARIA_LABEL: 'Aria Label' 31 | ANCHORS_CLASS: 'Custom CSS classes for anchors' 32 | ANCHORS_CLASS_HELP: 'Any custom classes to add to the anchor tags' 33 | ANCHORS_ICON: 'Anchor icon symbol' 34 | ANCHORS_ICON_HELP: 'Can be any text character, symbol, unicode character or even emjoi. leave blank if you intend to style with CSS.' 35 | ANCHORS_POSITION: 'Anchor Position' 36 | ANCHORS_POSITION_HELP: 'Position to put the anchor, `before|after`' 37 | ANCHORS_COPY_TO_CLIPBOARD: 'Copy to Clipboard' 38 | ANCHORS_COPY_TO_CLIPBOARD_HELP: 'When clicking an anchor, it will also copy to clipboard the full URL. Convenient for sharing/opening in new tab' 39 | BEFORE_TEXT: 'Before text' 40 | AFTER_TEXT: 'After text' 41 | SLUG_MAXLEN: 'Slug max-length' 42 | SLUG_MAXLEN_HELP: 'Max length of slugs used for anchors' 43 | SLUG_PREFIX: 'Slug prefix' 44 | SLUG_PREFIX_HELP: 'A prefix used in front of generated slugs' 45 | PAGE_ANCHORS_SECTION: 'Page Anchors Configuration' 46 | COPY_TO_CLIPBOARD: 'Copy to Clipboard' 47 | fr: 48 | PLUGIN_PAGE_TOC: 49 | TABLE_OF_CONTENTS: Table des matières 50 | INCLUDE_CSS: 'Inclure le CSS' 51 | ACTIVE_BY_DEFAULT: 'Ancres générées par défaut' 52 | ACTIVE_BY_DEFAULT_PAGE: 'Ancres générées sur cette page' 53 | ACTIVE_BY_DEFAULT_PAGE_HELP: 'Si les paramètres par défaut sont désactivés, vous pouvez les activer sur cette page ou vice-versa' 54 | TOC_SECTION: 'Configuration de la table des matières' 55 | START_TOC_HEADERS: 'Début des titres de la table des matières' 56 | START_TOC_HEADERS_HELP: 'Le niveau de titre de référence de la table des matières' 57 | DEPTH_TOC_HEADERS: 'Profondeur des titres de la table des matières' 58 | DEPTH_TOC_HEADERS_HELP: 'Le nombre de sous-niveaux de titres dans la table des matières, à partir du niveau de référence' 59 | HEADER_CSS_CLASSES: 'Classes CSS à utiliser pour les titres' 60 | HEADER_CSS_CLASSES_HELP: 'Classes CSS à ajouter aux tags des titres quand les identifiants de slugs sont ajoutés' 61 | ANCHORS_SECTION: 'Configuration des ancres' 62 | START_ANCHOR_HEADERS: 'Début des ancres de titres' 63 | DEPTH_ANCHOR_HEADERS: 'Profondeur des ancres de titres' 64 | DEPTH_ANCHOR_HEADERS_HELP: 'Le nombre de niveaux de titres depuis le ''début'' à afficher' 65 | LINK_ANCHOR_HEADERS: 'Liens des ancres' 66 | ARIA_LABEL: 'Aria Label' 67 | ANCHORS_CLASS: 'Classes CSS personnalisées des ancres' 68 | ANCHORS_CLASS_HELP: 'Classes CSS à ajouter aux tags des ancres' 69 | ANCHORS_ICON: 'Symbole utilisé pour les icônes d''ancres' 70 | ANCHORS_ICON_HELP: 'Peut être n''importe quel caractère, symbole, caractère unicode ou même un émoticône. Laissez ce champ vide si vous comptez utiliser du CSS.' 71 | ANCHORS_POSITION: 'Position des ancres' 72 | ANCHORS_POSITION_HELP: 'Position où afficher les ancres: avant ou après (valeurs acceptées: `before|after`)' 73 | ANCHORS_COPY_TO_CLIPBOARD: 'Copier dans le presse-papiers' 74 | ANCHORS_COPY_TO_CLIPBOARD_HELP: 'Lors du clic sur une ancre, cela copie également l''URL complète dans le presse-papiers. Utile pour partager/ouvrir dans un nouvel onglet' 75 | BEFORE_TEXT: 'Avant le texte' 76 | AFTER_TEXT: 'Après le texte' 77 | SLUG_MAXLEN: 'Longueur maximale des slugs' 78 | SLUG_MAXLEN_HELP: 'Longueur maximale des slugs utilisés pour les ancres' 79 | SLUG_PREFIX: 'Préfixe des slugs' 80 | SLUG_PREFIX_HELP: 'Un préfixe utilisé devant les slugs générés' 81 | PAGE_ANCHORS_SECTION: 'Configuration des ancres de pages' 82 | COPY_TO_CLIPBOARD: 'Copier dans le presse-papiers' 83 | es: 84 | PLUGIN_PAGE_TOC: 85 | TABLE_OF_CONTENTS: Tabla de Contenidos 86 | INCLUDE_CSS: 'Incluir CSS' 87 | ACTIVE_BY_DEFAULT: 'Enlaces ancla generados por defecto' 88 | ACTIVE_BY_DEFAULT_PAGE: 'Enlaces ancla generados para esta página' 89 | ACTIVE_BY_DEFAULT_PAGE_HELP: 'Si la configuración por defecto está desactivada, puedes activarla en esta página, o viceversa' 90 | ACTIVE_FOR_TEMPLATES: 'Enlaces ancla generados para estas plantillas' 91 | ACTIVE_FOR_TEMPLATES_HELP: 'Solo es relevante si la generación de enlaces ancla predeterminada (para todo el sitio) está desactivada' 92 | TOC_SECTION: 'Configuración de la Tabla de Contenidos' 93 | START_TOC_HEADERS: 'Iniciar encabezados de TOC' 94 | START_TOC_HEADERS_HELP: 'El nivel de encabezado desde el cual iniciar la TOC' 95 | DEPTH_TOC_HEADERS: 'Profundidad de encabezados de TOC' 96 | DEPTH_TOC_HEADERS_HELP: 'El número de niveles de encabezados desde el ''inicio'' a incluir en la TOC' 97 | HEADER_CSS_CLASSES: 'Clases CSS de encabezado' 98 | HEADER_CSS_CLASSES_HELP: 'Cualquier clase personalizada para agregar a las etiquetas de encabezado cuando se agreguen ID para los slugs' 99 | ALLOWED_HTML_TAGS: 'Etiquetas HTML permitidas' 100 | ALLOWED_HTML_TAGS_HELP: 'Estas etiquetas, si están presentes en los encabezados, se conservarán en la TOC' 101 | ANCHORS_SECTION: 'Configuración de enlaces ancla' 102 | START_ANCHOR_HEADERS: 'Iniciar encabezados de enlace ancla' 103 | DEPTH_ANCHOR_HEADERS: 'Profundidad de encabezados de enlace ancla' 104 | DEPTH_ANCHOR_HEADERS_HELP: 'El número de niveles de encabezados desde el ''inicio'' que se mostrarán' 105 | LINK_ANCHOR_HEADERS: 'Enlaces de anclaje' 106 | ARIA_LABEL: 'Etiqueta Aria' 107 | ANCHORS_CLASS: 'Clases CSS personalizadas para enlaces ancla' 108 | ANCHORS_CLASS_HELP: 'Cualquier clase personalizada para agregar a las etiquetas de enlace ancla' 109 | ANCHORS_ICON: 'Símbolo del icono de enlace ancla' 110 | ANCHORS_ICON_HELP: 'Puede ser cualquier carácter de texto, símbolo, carácter unicode o incluso emoji. déjalo en blanco si tienes la intención de estilizar con CSS.' 111 | ANCHORS_POSITION: 'Posición del enlace ancla' 112 | ANCHORS_POSITION_HELP: 'Posición para poner el enlace ancla, `antes|después`' 113 | ANCHORS_COPY_TO_CLIPBOARD: 'Copiar al Portapapeles' 114 | ANCHORS_COPY_TO_CLIPBOARD_HELP: 'Al hacer clic en un enlace ancla, también copiará al portapapeles la URL completa. Conveniente para compartir/abrir en una nueva pestaña' 115 | BEFORE_TEXT: 'Texto antes' 116 | AFTER_TEXT: 'Texto después' 117 | SLUG_MAXLEN: 'Longitud máxima del Slug' 118 | SLUG_MAXLEN_HELP: 'Longitud máxima de los slugs usados para los enlaces ancla' 119 | SLUG_PREFIX: 'Prefijo del Slug' 120 | SLUG_PREFIX_HELP: 'Un prefijo usado delante de los slugs generados' 121 | PAGE_ANCHORS_SECTION: 'Configuración de enlaces ancla de Página' 122 | COPY_TO_CLIPBOARD: 'Copiar al Portapapeles' 123 | 124 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 3.5 (2024-03-23) 2 | 3 | * Added CallbackVoter 4 | * Removed support for unsupported PHP version 8.0 5 | 6 | ## 3.4 (2023-05-17) 7 | 8 | * Removed support for unsupported PHP version 7.4 9 | 10 | ## 3.3 (2021-10-28) 11 | 12 | * Added support for Symfony 6 13 | 14 | ## 3.2 (2021-05-28) 15 | 16 | * Remove Symfony 6 deprecations 17 | * Enforce phpstan rules (max level) 18 | 19 | ## 3.1 (2019-12-01) 20 | 21 | * Allowed Symfony 5 components 22 | * Removed support for unsupported Symfony versions (4.0 and 4.1) 23 | * Allowed Twig 3 24 | 25 | ## 3.0 (2019-09-02) 26 | 27 | * Raised PHP requirements 28 | * [BC break] Enforced strong types on all interfaces and classes 29 | * [BC break] Removed deprecated features. Specifically, MenuFactory and MenuItem are not accepting a `null` name anymore 30 | 31 | ## 2.4 (2019-07-29) 32 | 33 | * Fixed Twig deprecations 34 | * Switched to namespaced Twig 35 | * Fixed sprintf use 36 | 37 | ## 2.3 (2017-11-18) 38 | 39 | * Deprecated the Silex 1 KnpMenuServiceProvider. Use the `knplabs/knp-menu-silex` package instead. 40 | * Fixed RouteVoter to also match on non-string request arguments like integers as long as both string representations are identical. 41 | * Add Symfony 4 support 42 | 43 | ## 2.2 (2016-09-22) 44 | 45 | * Added a new function to twig: `knp_menu_get_current_item` 46 | 47 | ## 2.1.1 (2016-01-08) 48 | 49 | * Made compatible with Symfony 3 50 | 51 | ## 2.1.0 (2015-09-20) 52 | 53 | * Added a new function to twig: `knp_menu_get_breadcrumbs_array` 54 | * Added a new filter to twig: `knp_menu_as_string` 55 | * Added 2 new tests to twig: `knp_menu_current`, `knp_menu_ancestor` 56 | * Made the templates compatible with Twig 2 57 | * Add menu and renderer providers supporting any ArrayAccess implementations. The 58 | Pimple-based providers (supporting only Pimple 1) are dperecated in favor of these 59 | new providers. 60 | 61 | ## 2.0.1 (2014-08-01) 62 | 63 | * Fixed voter conventions on RouteVoter 64 | 65 | ## 2.0.0 (2014-07-18) 66 | 67 | * [BC break] Clean code and removed the BC layer 68 | 69 | ## 2.0.0 beta 1 (2014-06-19) 70 | 71 | * [BC break] Added the new `Integration` namespace and removed the `Silex` one. 72 | * Added a new Voter based on regular expression: `Knp\Menu\Matcher\Voter\RegexVoter` 73 | 74 | ## 2.0.0 alpha 2 (2014-05-01) 75 | 76 | * [BC break] Changed the TwigRenderer to accept a menu template only as a string 77 | * [BC break] Refactored the way of rendering twig templates. Every template should extends 78 | the `knp_menu.html.twig` template. 79 | * Introduced extension points in the MenuFactory through `Knp\Menu\Factory\ExtensionInterface` 80 | * [BC break compared to 2.0 alpha 1] The inheritance extension points introduced in alpha1 are 81 | deprecated in favor of extensions and will be removed before the stable release. 82 | * `Knp\Menu\Silex\RouterAwareFactory` is deprecated in favor of `Knp\Menu\Silex\RoutingExtension`. 83 | * [BC break] Deprecated the methods `createFromArray` and `createFromNode` in the MenuFactory and 84 | removed them from `Knp\Menu\FactoryInterface`. Use `Knp\Menu\Loader\ArrayLoader` and 85 | `Knp\Menu\Loader\NodeLoader` instead. 86 | * [BC break] Deprecated the methods `moveToPosition`, `moveToFirstPosition`, `moveToLastPosition`, 87 | `moveChildToPosition`, `callRecursively`, `toArray`, `getPathAsString` and `getBreadcrumbsArray` 88 | in the MenuItem and removed them from `Knp\Menu\ItemInterface`. Use `Knp\Menu\Util\MenuManipulator` 89 | instead. 90 | * Made the RouterVoter compatible with SensioFrameworkExtraBundle param converters 91 | * Added the possibility to match routes using a regex on their name in the RouterVoter 92 | * [BC break compared to 2.0 alpha 1] Refactored the RouterVoter to make it more flexible 93 | The way to pass routes in the item extras has changed. 94 | 95 | Before: 96 | 97 | ```php 98 | 'extras' => array( 99 | 'routes' => array('foo', 'bar'), 100 | 'routeParameters' => array('foo' => array('id' => 4)), 101 | ) 102 | ``` 103 | 104 | After: 105 | 106 | ```php 107 | 'extras' => array( 108 | 'routes' => array( 109 | array('route' => 'foo', 'parameters' => array('id' => 4)), 110 | 'bar', 111 | ) 112 | ) 113 | ``` 114 | 115 | The old syntax is kept until the final release, but using it will trigger a E_USER_DEPRECATED error. 116 | 117 | ## 2.0.0 alpha 1 (2013-06-23) 118 | 119 | * Added protected methods `buildOptions` and `configureItem` in the MenuFactory as extension point by inheritance 120 | * [BC break] Refactored the way to mark items as current 121 | ``setCurrentUri``, ``getCurrentUri`` and ``getCurrentItem`` have been removed from the ItemInterface. 122 | Determining the current items is now delegated to a matcher, and the default implementation 123 | uses voters to apply the matching. Getting the current items can be done thanks to the CurrentItemFilterIterator. 124 | * [BC break] The signature of the CurrentItemFilterIterator constructor changed to accept the item matcher 125 | * [BC break] Changed the format of the breadcrumb array 126 | Instead of storing the elements with the label as key and the uri as value 127 | the array now stores an array of array elements with 3 keys: `label`, `uri` and `item`. 128 | 129 | ## 1.1.2 (2012-06-10) 130 | 131 | * Updated the Silex service provider for the change in the interface 132 | 133 | ## 1.1.1 (2012-05-17) 134 | 135 | * Added the children attributes and the extras in the array export 136 | 137 | ## 1.1.0 (2012-05-17) 138 | 139 | * Marked `Knp\Menu\ItemInterface::getCurrentItem` as deprecated 140 | * Added a recursive filter iterator keeping only displayed items 141 | * Added a filter iterator keeping only current items 142 | * Added a recursive iterator for the item 143 | * Fixed building an array of breadcrumbs when a label has only digits 144 | * Added a way to mark a label as safe 145 | * Refactored the ListRenderer to be consistent with the TwigRenderer and provide the same extension points 146 | * Added a way to attach extra data to an item 147 | * Removed unnecessary optimization in the TwigRenderer 148 | * Added some whitespace control in the Twig template to ensure an empty rendering is really empty 149 | * [BC break] Use the childrenAttributes for the root instead of the attributes 150 | * Made the default options configurable for the TwigRenderer 151 | * Added the support for menu registered as factory in PimpleProvider 152 | * Added a way to use the options in `knp_menu_get()` in Twig templates 153 | * Added an array of options for the MenuProviderInterface 154 | * Added a template to render an ordered list 155 | * Refactored the template a bit to make it easier to use an ordered list 156 | * Allow omitting the name of the child in `fromArray` (the key is used instead) 157 | 158 | ## 1.0.0 (2011-12-03) 159 | 160 | * Add composer.json file 161 | * Added more flexible list element blocks 162 | * Add support for attributes on the children collection. 163 | * Added a default renderer 164 | * Added a ChainProvider for the menus. 165 | * Added the Silex extension 166 | * Added a RouterAwareFactory 167 | * Added an helper to be able to reuse the logic more easily for other templating engines 168 | * Added a way to retrieve an item using a path in a menu tree 169 | * Changed the toArray method to use a depth instead of simply using a boolean flag 170 | * Refactored the export to array and the creation from an array 171 | * Added better support for encoding problems when escaping a string in the ListRenderer 172 | * Added a Twig renderer 173 | * Added missing escaping in the ListRenderer 174 | * Renamed some methods in the ItemInterface 175 | * Removed the configuration of the current item as link from the item 176 | * Refactored the ListRenderer to use options 177 | * Changed the interface of callRecursively 178 | * Refactored the NodeInterface to be consistent 179 | * Moved the creation of the item to the factory 180 | * Added a Twig extension to render the menu easily 181 | * Changed the menu provider interface with a pimple-based implementation 182 | * Added a renderer provider to get a renderer by name and a Pimple-based implementation 183 | * Removed the renderer from the menu 184 | * Removed the num in the item by refactoring isLast and isFirst 185 | * Changed the RendererInterface to accept an array of options to be more flexible 186 | * Added an ItemInterface 187 | * Initial import of KnpMenuBundle decoupled classes with a new namespace 188 | -------------------------------------------------------------------------------- /page-toc.php: -------------------------------------------------------------------------------- 1 | ['onPluginsInitialized', 0] 43 | ]; 44 | } 45 | 46 | /** 47 | * Composer autoload 48 | * 49 | * @return ClassLoader 50 | */ 51 | public function autoload(): ClassLoader 52 | { 53 | return require __DIR__ . '/vendor/autoload.php'; 54 | } 55 | 56 | /** 57 | * Initialize the plugin 58 | */ 59 | public function onPluginsInitialized() 60 | { 61 | // Don't proceed if we are in the admin plugin 62 | if ($this->isAdmin()) { 63 | $this->enable([ 64 | 'onBlueprintCreated' => ['onBlueprintCreated', 0], 65 | ]); 66 | return; 67 | } 68 | 69 | // Enable the main event we are interested in 70 | $this->enable([ 71 | 'onShortcodeHandlers' => ['onShortcodeHandlers', 0], 72 | 'onTwigInitialized' => ['onTwigInitialized', 0], 73 | 'onTwigTemplatePaths' => ['onTwigTemplatePaths', 0], 74 | 'onTwigSiteVariables' => ['onTwigSiteVariables', 0], 75 | 'onPageContentProcessed' => ['onPageContentProcessed', -20], 76 | ]); 77 | } 78 | 79 | public function onShortcodeHandlers() 80 | { 81 | $this->grav['shortcode']->registerAllShortcodes(__DIR__ . '/classes/shortcodes'); 82 | } 83 | 84 | public function onPageContentProcessed(Event $event) 85 | { 86 | /** @var PageInterface $page */ 87 | $page = $event['page']; 88 | 89 | $content = $page->content(); 90 | $shortcode_exists = preg_match($this->toc_regex, $content); 91 | $active = $this->configVar('active', $page, false); 92 | $activated_templates = $this->configVar('templates', $page, []); 93 | $is_template_activated = in_array($page->template(), $activated_templates); 94 | 95 | // Set ID anchors if needed 96 | if ($active || $is_template_activated || $shortcode_exists) { 97 | $this->registerTwigFunctions(); 98 | $markup_fixer = new MarkupFixer(); 99 | $content = $markup_fixer->fix($content, $this->getAnchorOptions($page)); 100 | $page->setRawContent($content); 101 | } 102 | 103 | // Replace shortcode if found 104 | if ($shortcode_exists) { 105 | $toc = $this->grav['twig']->processTemplate('components/page-toc.html.twig', ['page' => $page, 'active' => true]); 106 | $content = preg_replace($this->toc_regex, $toc, $content); 107 | $page->setRawContent($content); 108 | } 109 | } 110 | 111 | public function onTwigInitialized() 112 | { 113 | $this->registerTwigFunctions(); 114 | } 115 | 116 | public function onTwigSiteVariables() 117 | { 118 | if ($this->grav['config']->get('plugins.page-toc.include_css')) { 119 | $this->grav['assets']->addCss('plugin://page-toc/assets/page-toc-anchors.css'); 120 | } 121 | if ($this->grav['config']->get('plugins.page-toc.anchors.copy_to_clipboard')) { 122 | $this->grav['assets']->addJs('plugin://page-toc/assets/page-toc-anchors.js', ['group' => 'bottom', 'defer' => 'defer']); 123 | } 124 | } 125 | 126 | public function registerTwigFunctions() 127 | { 128 | static $functions_registered; 129 | 130 | if ($functions_registered) { 131 | return; 132 | } 133 | 134 | $this->generator = new TocGenerator(); 135 | $this->fixer = new MarkupFixer(); 136 | $twig = $this->grav['twig']->twig(); 137 | 138 | $twig->addFunction(new TwigFunction('toc', function ($markup, $start = null, $depth = null) { 139 | $options = $this->getTocOptions(null, $start, $depth); 140 | return $this->generator->getHtmlMenu($markup, $options['start'], $options['depth']); 141 | }, ['is_safe' => ['html']])); 142 | 143 | $twig->addFunction(new TwigFunction('toc_ordered', function ($markup, $start = null, $depth = null) { 144 | $options = $this->getTocOptions(null, $start, $depth); 145 | return $this->generator->getHtmlMenu($markup, $options['start'], $options['depth'], null, true); 146 | }, ['is_safe' => ['html']])); 147 | 148 | $twig->addFunction(new TwigFunction('toc_items', function ($markup, $start = null, $depth = null, $tags = null) { 149 | $options = $this->getTocOptions(null, $start, $depth, $tags); 150 | return $this->generator->getMenu($markup, $options['start'], $options['depth'], $options['tags']); 151 | })); 152 | 153 | $twig->addFunction(new TwigFunction('add_anchors', function ($markup, $start = null, $depth = null) { 154 | $options = $this->getAnchorOptions(null, $start, $depth); 155 | return $this->fixer->fix($markup, $options); 156 | }, ['is_safe' => ['html']])); 157 | 158 | $twig->addFunction(new TwigFunction('toc_config_var', function ($var) { 159 | return static::configVar($var); 160 | })); 161 | 162 | $functions_registered = true; 163 | } 164 | 165 | public function onTwigTemplatePaths() 166 | { 167 | $this->grav['twig']->twig_paths[] = __DIR__ . '/templates'; 168 | } 169 | 170 | /** 171 | * Extend page blueprints with TOC options. 172 | * 173 | * @param Event $event 174 | */ 175 | public function onBlueprintCreated(Event $event) 176 | { 177 | static $inEvent = false; 178 | 179 | /** @var Data\Blueprint $blueprint */ 180 | $blueprint = $event['blueprint']; 181 | $form = $blueprint->form(); 182 | 183 | $advanced_tab_exists = isset($form['fields']['tabs']['fields']['advanced']); 184 | 185 | if (!$inEvent && $advanced_tab_exists) { 186 | $inEvent = true; 187 | $blueprints = new Data\Blueprints(__DIR__ . '/blueprints/'); 188 | $extends = $blueprints->get('page-toc'); 189 | $blueprint->extend($extends, true); 190 | $inEvent = false; 191 | } 192 | } 193 | 194 | protected function getTocOptions(PageInterface $page = null, $start = null, $depth = null, $tags = null): array 195 | { 196 | $page = $page ?? $this->grav['page']; 197 | return [ 198 | 'start' => $start ?? $this->configVar('start', $page,1), 199 | 'depth' => $depth ?? $this->configVar('depth', $page,6), 200 | 'tags' => $tags ?? $this->configVar('tags', $page,[]), 201 | ]; 202 | } 203 | 204 | protected function getAnchorOptions(PageInterface $page = null, $start = null, $depth = null): array 205 | { 206 | $page = $page ?? $this->grav['page']; 207 | return [ 208 | 'start' => (int) ($start ?? $this->configVar('anchors.start', $page,1)), 209 | 'depth' => (int) ($depth ?? $this->configVar('anchors.depth', $page,6)), 210 | 'hclass' => $this->configVar('hclass', $page,null), 211 | 'link' => $this->configVar('anchors.link', $page,true), 212 | 'position' => $this->configVar('anchors.position', $page,'before'), 213 | 'aria' => $this->configVar('anchors.aria', $page,'Anchor'), 214 | 'icon' => $this->configVar('anchors.icon', $page,'#'), 215 | 'class' => $this->configVar('anchors.class', $page,null), 216 | 'maxlen' => (int) ($this->configVar('anchors.slug_maxlen', $page,null)), 217 | 'prefix' => $this->configVar('anchors.slug_prefix', $page,null), 218 | ]; 219 | } 220 | 221 | public static function configVar($var, $page = null, $default = null) 222 | { 223 | return Plugin::inheritedConfigOption('page-toc', $var, $page, $default); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Renderer/ListRenderer.php: -------------------------------------------------------------------------------- 1 | $defaultOptions 15 | */ 16 | public function __construct(protected MatcherInterface $matcher, protected array $defaultOptions = [], ?string $charset = null) 17 | { 18 | $this->defaultOptions = \array_merge([ 19 | 'depth' => null, 20 | 'matchingDepth' => null, 21 | 'currentAsLink' => true, 22 | 'currentClass' => 'current', 23 | 'ancestorClass' => 'current_ancestor', 24 | 'firstClass' => 'first', 25 | 'lastClass' => 'last', 26 | 'compressed' => false, 27 | 'allow_safe_labels' => false, 28 | 'clear_matcher' => true, 29 | 'leaf_class' => null, 30 | 'branch_class' => null, 31 | ], $defaultOptions); 32 | 33 | parent::__construct($charset); 34 | } 35 | 36 | public function render(ItemInterface $item, array $options = []): string 37 | { 38 | $options = \array_merge($this->defaultOptions, $options); 39 | 40 | $html = $this->renderList($item, $item->getChildrenAttributes(), $options); 41 | 42 | if ($options['clear_matcher']) { 43 | $this->matcher->clear(); 44 | } 45 | 46 | return $html; 47 | } 48 | 49 | /** 50 | * @param array $attributes 51 | * @param array $options 52 | */ 53 | protected function renderList(ItemInterface $item, array $attributes, array $options): string 54 | { 55 | /* 56 | * Return an empty string if any of the following are true: 57 | * a) The menu has no children eligible to be displayed 58 | * b) The depth is 0 59 | * c) This menu item has been explicitly set to hide its children 60 | */ 61 | if (0 === $options['depth'] || !$item->hasChildren() || !$item->getDisplayChildren()) { 62 | return ''; 63 | } 64 | 65 | $html = $this->format('renderHtmlAttributes($attributes).'>', 'ul', $item->getLevel(), $options); 66 | $html .= $this->renderChildren($item, $options); 67 | $html .= $this->format('', 'ul', $item->getLevel(), $options); 68 | 69 | return $html; 70 | } 71 | 72 | /** 73 | * Renders all of the children of this menu. 74 | * 75 | * This calls ->renderItem() on each menu item, which instructs each 76 | * menu item to render themselves as an
  • tag (with nested ul if it 77 | * has children). 78 | * This method updates the depth for the children. 79 | * 80 | * @param array $options the options to render the item 81 | */ 82 | protected function renderChildren(ItemInterface $item, array $options): string 83 | { 84 | // render children with a depth - 1 85 | if (null !== $options['depth']) { 86 | --$options['depth']; 87 | } 88 | 89 | if (null !== $options['matchingDepth'] && $options['matchingDepth'] > 0) { 90 | --$options['matchingDepth']; 91 | } 92 | 93 | $html = ''; 94 | foreach ($item->getChildren() as $child) { 95 | $html .= $this->renderItem($child, $options); 96 | } 97 | 98 | return $html; 99 | } 100 | 101 | /** 102 | * Called by the parent menu item to render this menu. 103 | * 104 | * This renders the li tag to fit into the parent ul as well as its 105 | * own nested ul tag if this menu item has children 106 | * 107 | * @param array $options The options to render the item 108 | */ 109 | protected function renderItem(ItemInterface $item, array $options): string 110 | { 111 | // if we don't have access or this item is marked to not be shown 112 | if (!$item->isDisplayed()) { 113 | return ''; 114 | } 115 | 116 | // create an array than can be imploded as a class list 117 | $class = (array) $item->getAttribute('class'); 118 | 119 | if ($this->matcher->isCurrent($item)) { 120 | $class[] = $options['currentClass']; 121 | } elseif ($this->matcher->isAncestor($item, $options['matchingDepth'])) { 122 | $class[] = $options['ancestorClass']; 123 | } 124 | 125 | if ($item->actsLikeFirst()) { 126 | $class[] = $options['firstClass']; 127 | } 128 | if ($item->actsLikeLast()) { 129 | $class[] = $options['lastClass']; 130 | } 131 | 132 | if (0 !== $options['depth'] && $item->hasChildren()) { 133 | if (null !== $options['branch_class'] && $item->getDisplayChildren()) { 134 | $class[] = $options['branch_class']; 135 | } 136 | } elseif (null !== $options['leaf_class']) { 137 | $class[] = $options['leaf_class']; 138 | } 139 | 140 | // retrieve the attributes and put the final class string back on it 141 | $attributes = $item->getAttributes(); 142 | if (!empty($class)) { 143 | $attributes['class'] = \implode(' ', $class); 144 | } 145 | 146 | // opening li tag 147 | $html = $this->format('renderHtmlAttributes($attributes).'>', 'li', $item->getLevel(), $options); 148 | 149 | // render the text/link inside the li tag 150 | // $html .= $this->format($item->getUri() ? $item->renderLink() : $item->renderLabel(), 'link', $item->getLevel()); 151 | $html .= $this->renderLink($item, $options); 152 | 153 | // renders the embedded ul 154 | $childrenClass = (array) $item->getChildrenAttribute('class'); 155 | $childrenClass[] = 'menu_level_'.$item->getLevel(); 156 | 157 | $childrenAttributes = $item->getChildrenAttributes(); 158 | $childrenAttributes['class'] = \implode(' ', $childrenClass); 159 | 160 | $html .= $this->renderList($item, $childrenAttributes, $options); 161 | 162 | // closing li tag 163 | $html .= $this->format('
  • ', 'li', $item->getLevel(), $options); 164 | 165 | return $html; 166 | } 167 | 168 | /** 169 | * Renders the link in a a tag with link attributes or 170 | * the label in a span tag with label attributes 171 | * 172 | * Tests if item has a an uri and if not tests if it's 173 | * the current item and if the text has to be rendered 174 | * as a link or not. 175 | * 176 | * @param ItemInterface $item The item to render the link or label for 177 | * @param array $options The options to render the item 178 | */ 179 | protected function renderLink(ItemInterface $item, array $options = []): string 180 | { 181 | if (null !== $item->getUri() && (!$this->matcher->isCurrent($item) || $options['currentAsLink'])) { 182 | $text = $this->renderLinkElement($item, $options); 183 | } else { 184 | $text = $this->renderSpanElement($item, $options); 185 | } 186 | 187 | return $this->format($text, 'link', $item->getLevel(), $options); 188 | } 189 | 190 | /** 191 | * @param array $options 192 | */ 193 | protected function renderLinkElement(ItemInterface $item, array $options): string 194 | { 195 | \assert(null !== $item->getUri()); 196 | 197 | return \sprintf('%s', $this->escape($item->getUri()), $this->renderHtmlAttributes($item->getLinkAttributes()), $this->renderLabel($item, $options)); 198 | } 199 | 200 | /** 201 | * @param array $options 202 | */ 203 | protected function renderSpanElement(ItemInterface $item, array $options): string 204 | { 205 | return \sprintf('%s', $this->renderHtmlAttributes($item->getLabelAttributes()), $this->renderLabel($item, $options)); 206 | } 207 | 208 | /** 209 | * @param array $options 210 | */ 211 | protected function renderLabel(ItemInterface $item, array $options): string 212 | { 213 | if ($options['allow_safe_labels'] && $item->getExtra('safe_label', false)) { 214 | return $item->getLabel(); 215 | } 216 | 217 | return $this->escape($item->getLabel()); 218 | } 219 | 220 | /** 221 | * If $this->renderCompressed is on, this will apply the necessary 222 | * spacing and line-breaking so that the particular thing being rendered 223 | * makes up its part in a fully-rendered and spaced menu. 224 | * 225 | * @param string $html The html to render in an (un)formatted way 226 | * @param string $type The type [ul,link,li] of thing being rendered 227 | * @param array $options 228 | */ 229 | protected function format(string $html, string $type, int $level, array $options): string 230 | { 231 | if ($options['compressed']) { 232 | return $html; 233 | } 234 | 235 | $spacing = 0; 236 | 237 | switch ($type) { 238 | case 'ul': 239 | case 'link': 240 | $spacing = $level * 4; 241 | break; 242 | 243 | case 'li': 244 | $spacing = $level * 4 - 2; 245 | } 246 | 247 | return \str_repeat(' ', $spacing).$html."\n"; 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/ItemInterface.php: -------------------------------------------------------------------------------- 1 | tag and is what you should interact with 9 | * most of the time by default. 10 | * Originally taken from ioMenuPlugin (http://github.com/weaverryan/ioMenuPlugin) 11 | * 12 | * @extends \ArrayAccess 13 | * @extends \IteratorAggregate 14 | */ 15 | interface ItemInterface extends \ArrayAccess, \Countable, \IteratorAggregate 16 | { 17 | public function setFactory(FactoryInterface $factory): self; 18 | 19 | public function getName(): string; 20 | 21 | /** 22 | * Renames the item. 23 | * 24 | * This method must also update the key in the parent. 25 | * 26 | * Provides a fluent interface 27 | * 28 | * @throws \InvalidArgumentException if the name is already used by a sibling 29 | */ 30 | public function setName(string $name): self; 31 | 32 | /** 33 | * Get the uri for a menu item 34 | */ 35 | public function getUri(): ?string; 36 | 37 | /** 38 | * Set the uri for a menu item 39 | * 40 | * Provides a fluent interface 41 | * 42 | * @param string|null $uri The uri to set on this menu item 43 | */ 44 | public function setUri(?string $uri): self; 45 | 46 | /** 47 | * Returns the label that will be used to render this menu item 48 | * 49 | * Defaults to the name of no label was specified 50 | */ 51 | public function getLabel(): string; 52 | 53 | /** 54 | * Provides a fluent interface 55 | * 56 | * @param string|null $label The text to use when rendering this menu item 57 | */ 58 | public function setLabel(?string $label): self; 59 | 60 | /** 61 | * @return array 62 | */ 63 | public function getAttributes(): array; 64 | 65 | /** 66 | * @param array $attributes 67 | */ 68 | public function setAttributes(array $attributes): self; 69 | 70 | /** 71 | * @param string $name The name of the attribute to return 72 | * @param string|bool|null $default The value to return if the attribute doesn't exist 73 | * 74 | * @return string|bool|null 75 | */ 76 | public function getAttribute(string $name, $default = null); 77 | 78 | /** 79 | * @param string|bool|null $value 80 | */ 81 | public function setAttribute(string $name, $value): self; 82 | 83 | /** 84 | * @return array 85 | */ 86 | public function getLinkAttributes(): array; 87 | 88 | /** 89 | * @param array $linkAttributes 90 | */ 91 | public function setLinkAttributes(array $linkAttributes): self; 92 | 93 | /** 94 | * @param string $name The name of the attribute to return 95 | * @param string|bool|null $default The value to return if the attribute doesn't exist 96 | * 97 | * @return string|bool|null 98 | */ 99 | public function getLinkAttribute(string $name, $default = null); 100 | 101 | /** 102 | * @param string|bool|null $value 103 | */ 104 | public function setLinkAttribute(string $name, $value): self; 105 | 106 | /** 107 | * @return array 108 | */ 109 | public function getChildrenAttributes(): array; 110 | 111 | /** 112 | * @param array $childrenAttributes 113 | */ 114 | public function setChildrenAttributes(array $childrenAttributes): self; 115 | 116 | /** 117 | * @param string $name The name of the attribute to return 118 | * @param string|bool|null $default The value to return if the attribute doesn't exist 119 | * 120 | * @return string|bool|null 121 | */ 122 | public function getChildrenAttribute(string $name, $default = null); 123 | 124 | /** 125 | * @param string|bool|null $value 126 | */ 127 | public function setChildrenAttribute(string $name, $value): self; 128 | 129 | /** 130 | * @return array 131 | */ 132 | public function getLabelAttributes(): array; 133 | 134 | /** 135 | * @param array $labelAttributes 136 | */ 137 | public function setLabelAttributes(array $labelAttributes): self; 138 | 139 | /** 140 | * @param string $name The name of the attribute to return 141 | * @param string|bool|null $default The value to return if the attribute doesn't exist 142 | * 143 | * @return string|bool|null 144 | */ 145 | public function getLabelAttribute(string $name, $default = null); 146 | 147 | /** 148 | * @param string|bool|null $value 149 | */ 150 | public function setLabelAttribute(string $name, $value): self; 151 | 152 | /** 153 | * @return array 154 | */ 155 | public function getExtras(): array; 156 | 157 | /** 158 | * @param array $extras 159 | */ 160 | public function setExtras(array $extras): self; 161 | 162 | /** 163 | * @param string $name The name of the extra to return 164 | * @param mixed $default The value to return if the extra doesn't exist 165 | * 166 | * @return mixed 167 | */ 168 | public function getExtra(string $name, $default = null); 169 | 170 | /** 171 | * @param mixed $value 172 | */ 173 | public function setExtra(string $name, $value): self; 174 | 175 | public function getDisplayChildren(): bool; 176 | 177 | /** 178 | * Set whether or not this menu item should show its children 179 | * 180 | * Provides a fluent interface 181 | */ 182 | public function setDisplayChildren(bool $bool): self; 183 | 184 | /** 185 | * Whether or not to display this menu item 186 | */ 187 | public function isDisplayed(): bool; 188 | 189 | /** 190 | * Set whether or not this menu should be displayed 191 | * 192 | * Provides a fluent interface 193 | */ 194 | public function setDisplay(bool $bool): self; 195 | 196 | /** 197 | * Add a child menu item to this menu 198 | * 199 | * Returns the child item 200 | * 201 | * @param ItemInterface|string $child An ItemInterface instance or the name of a new item to create 202 | * @param array $options If creating a new item, the options passed to the factory for the item 203 | * 204 | * @throws \InvalidArgumentException if the item is already in a tree 205 | */ 206 | public function addChild($child, array $options = []): self; 207 | 208 | /** 209 | * Returns the child menu identified by the given name 210 | * 211 | * @param string $name Then name of the child menu to return 212 | */ 213 | public function getChild(string $name): ?self; 214 | 215 | /** 216 | * Reorder children. 217 | * 218 | * Provides a fluent interface 219 | * 220 | * @param array $order new order of children 221 | */ 222 | public function reorderChildren(array $order): self; 223 | 224 | /** 225 | * Makes a deep copy of menu tree. Every item is copied as another object. 226 | */ 227 | public function copy(): self; 228 | 229 | /** 230 | * Returns the level of this menu item 231 | * 232 | * The root menu item is 0, followed by 1, 2, etc 233 | */ 234 | public function getLevel(): int; 235 | 236 | /** 237 | * Returns the root ItemInterface of this menu tree 238 | */ 239 | public function getRoot(): self; 240 | 241 | /** 242 | * Returns whether or not this menu item is the root menu item 243 | */ 244 | public function isRoot(): bool; 245 | 246 | public function getParent(): ?self; 247 | 248 | /** 249 | * Used internally when adding and removing children 250 | * 251 | * Provides a fluent interface 252 | */ 253 | public function setParent(?self $parent = null): self; 254 | 255 | /** 256 | * Return the children as an array of ItemInterface objects 257 | * 258 | * @return array 259 | */ 260 | public function getChildren(): array; 261 | 262 | /** 263 | * Provides a fluent interface 264 | * 265 | * @param array $children An array of ItemInterface objects 266 | */ 267 | public function setChildren(array $children): self; 268 | 269 | /** 270 | * Removes a child from this menu item 271 | * 272 | * Provides a fluent interface 273 | * 274 | * @param ItemInterface|string $name The name of ItemInterface instance or the ItemInterface to remove 275 | */ 276 | public function removeChild($name): self; 277 | 278 | public function getFirstChild(): self; 279 | 280 | public function getLastChild(): self; 281 | 282 | /** 283 | * Returns whether or not this menu items has viewable children 284 | * 285 | * This menu MAY have children, but this will return false if the current 286 | * user does not have access to view any of those items 287 | */ 288 | public function hasChildren(): bool; 289 | 290 | /** 291 | * Sets whether or not this menu item is "current". 292 | * 293 | * If the state is unknown, use null. 294 | * 295 | * Provides a fluent interface 296 | * 297 | * @param bool|null $bool Specify that this menu item is current 298 | */ 299 | public function setCurrent(?bool $bool): self; 300 | 301 | /** 302 | * Gets whether or not this menu item is "current". 303 | */ 304 | public function isCurrent(): ?bool; 305 | 306 | /** 307 | * Whether this menu item is last in its parent 308 | */ 309 | public function isLast(): bool; 310 | 311 | /** 312 | * Whether this menu item is first in its parent 313 | */ 314 | public function isFirst(): bool; 315 | 316 | /** 317 | * Whereas isFirst() returns if this is the first child of the parent 318 | * menu item, this function takes into consideration whether children are rendered or not. 319 | * 320 | * This returns true if this is the first child that would be rendered 321 | * for the current user 322 | */ 323 | public function actsLikeFirst(): bool; 324 | 325 | /** 326 | * Whereas isLast() returns if this is the last child of the parent 327 | * menu item, this function takes into consideration whether children are rendered or not. 328 | * 329 | * This returns true if this is the last child that would be rendered 330 | * for the current user 331 | */ 332 | public function actsLikeLast(): bool; 333 | } 334 | -------------------------------------------------------------------------------- /vendor/knplabs/knp-menu/src/Knp/Menu/Util/MenuManipulator.php: -------------------------------------------------------------------------------- 1 | getParent()) { 17 | $this->moveChildToPosition($parent, $item, $position); 18 | } 19 | } 20 | 21 | /** 22 | * Moves child to specified position. Rearrange other children accordingly. 23 | * 24 | * @param ItemInterface $child Child to move 25 | * @param int $position Position to move child to 26 | */ 27 | public function moveChildToPosition(ItemInterface $item, ItemInterface $child, int $position): void 28 | { 29 | $name = $child->getName(); 30 | $order = \array_keys($item->getChildren()); 31 | 32 | $oldPosition = \array_search($name, $order); 33 | unset($order[$oldPosition]); 34 | 35 | $order = \array_values($order); 36 | 37 | \array_splice($order, $position, 0, $name); 38 | $item->reorderChildren($order); 39 | } 40 | 41 | /** 42 | * Moves item to first position. Rearrange siblings accordingly. 43 | */ 44 | public function moveToFirstPosition(ItemInterface $item): void 45 | { 46 | $this->moveToPosition($item, 0); 47 | } 48 | 49 | /** 50 | * Moves item to last position. Rearrange siblings accordingly. 51 | */ 52 | public function moveToLastPosition(ItemInterface $item): void 53 | { 54 | if (null !== $parent = $item->getParent()) { 55 | $this->moveToPosition($item, $parent->count()); 56 | } 57 | } 58 | 59 | /** 60 | * Get slice of menu as another menu. 61 | * 62 | * If offset and/or length are numeric, it works like in array_slice function: 63 | * 64 | * If offset is non-negative, slice will start at the offset. 65 | * If offset is negative, slice will start that far from the end. 66 | * 67 | * If length is null, slice will have all elements. 68 | * If length is positive, slice will have that many elements. 69 | * If length is negative, slice will stop that far from the end. 70 | * 71 | * It's possible to mix names/object/numeric, for example: 72 | * slice("child1", 2); 73 | * slice(3, $child5); 74 | * Note: when using a child as limit, it will not be included in the returned menu. 75 | * the slice is done before this menu. 76 | * 77 | * @param mixed $offset name of child, child object, or numeric offset 78 | * @param string|int|ItemInterface $length name of child, child object, or numeric length 79 | */ 80 | public function slice(ItemInterface $item, $offset, $length = null): ItemInterface 81 | { 82 | $names = \array_keys($item->getChildren()); 83 | if ($offset instanceof ItemInterface) { 84 | $offset = $offset->getName(); 85 | } 86 | if (!\is_int($offset)) { 87 | $offset = \array_search($offset, $names, true); 88 | if (false === $offset) { 89 | throw new \InvalidArgumentException('Not found.'); 90 | } 91 | } 92 | 93 | if (null !== $length) { 94 | if ($length instanceof ItemInterface) { 95 | $length = $length->getName(); 96 | } 97 | if (!\is_int($length)) { 98 | $index = \array_search($length, $names, true); 99 | $length = ($index < $offset) ? 0 : $index - $offset; 100 | } 101 | } 102 | 103 | $slicedItem = $item->copy(); 104 | $children = \array_slice($slicedItem->getChildren(), $offset, $length); 105 | $slicedItem->setChildren($children); 106 | 107 | return $slicedItem; 108 | } 109 | 110 | /** 111 | * Split menu into two distinct menus. 112 | * 113 | * @param string|int|ItemInterface $length name of child, child object, or numeric length 114 | * 115 | * @phpstan-return array{primary: ItemInterface, secondary: ItemInterface} 116 | * 117 | * @return array Array with two menus, with "primary" and "secondary" key 118 | */ 119 | public function split(ItemInterface $item, $length): array 120 | { 121 | return [ 122 | 'primary' => $this->slice($item, 0, $length), 123 | 'secondary' => $this->slice($item, $length), 124 | ]; 125 | } 126 | 127 | /** 128 | * Calls a method recursively on all of the children of this item 129 | * 130 | * @example 131 | * $menu->callRecursively('setShowChildren', [false]); 132 | * 133 | * @param array $arguments 134 | */ 135 | public function callRecursively(ItemInterface $item, string $method, array $arguments = []): void 136 | { 137 | $item->$method(...$arguments); 138 | 139 | foreach ($item->getChildren() as $child) { 140 | $this->callRecursively($child, $method, $arguments); 141 | } 142 | } 143 | 144 | /** 145 | * A string representation of this menu item 146 | * 147 | * e.g. Top Level > Second Level > This menu 148 | */ 149 | public function getPathAsString(ItemInterface $item, string $separator = ' > '): string 150 | { 151 | $children = []; 152 | $obj = $item; 153 | 154 | do { 155 | $children[] = $obj->getLabel(); 156 | } while ($obj = $obj->getParent()); 157 | 158 | return \implode($separator, \array_reverse($children)); 159 | } 160 | 161 | /** 162 | * @param int|null $depth the depth until which children should be exported (null means unlimited) 163 | * 164 | * @return array 165 | */ 166 | public function toArray(ItemInterface $item, ?int $depth = null): array 167 | { 168 | $array = [ 169 | 'name' => $item->getName(), 170 | 'label' => $item->getLabel(), 171 | 'uri' => $item->getUri(), 172 | 'attributes' => $item->getAttributes(), 173 | 'labelAttributes' => $item->getLabelAttributes(), 174 | 'linkAttributes' => $item->getLinkAttributes(), 175 | 'childrenAttributes' => $item->getChildrenAttributes(), 176 | 'extras' => $item->getExtras(), 177 | 'display' => $item->isDisplayed(), 178 | 'displayChildren' => $item->getDisplayChildren(), 179 | 'current' => $item->isCurrent(), 180 | ]; 181 | 182 | // export the children as well, unless explicitly disabled 183 | if (0 !== $depth) { 184 | $childDepth = null === $depth ? null : $depth - 1; 185 | $array['children'] = []; 186 | foreach ($item->getChildren() as $key => $child) { 187 | $array['children'][$key] = $this->toArray($child, $childDepth); 188 | } 189 | } 190 | 191 | return $array; 192 | } 193 | 194 | /** 195 | * Renders an array ready to be used for breadcrumbs. 196 | * 197 | * Each element in the array will be an array with 3 keys: 198 | * - `label` containing the label of the item 199 | * - `url` containing the url of the item (may be `null`) 200 | * - `item` containing the original item (may be `null` for the extra items) 201 | * 202 | * The subItem can be one of the following forms 203 | * * 'subItem' 204 | * * ItemInterface object 205 | * * ['subItem' => '@homepage'] 206 | * * ['subItem1', 'subItem2'] 207 | * * [['label' => 'subItem1', 'url' => '@homepage'], ['label' => 'subItem2']] 208 | * 209 | * @param string|ItemInterface|array|\Traversable $subItem A string or array to append onto the end of the array 210 | * 211 | * @phpstan-param string|ItemInterface|array|\Traversable $subItem 212 | * 213 | * @return array> 214 | * @phpstan-return list 215 | * 216 | * @throws \InvalidArgumentException if an element of the subItem is invalid 217 | */ 218 | public function getBreadcrumbsArray(ItemInterface $item, $subItem = null): array 219 | { 220 | $breadcrumbs = $this->buildBreadcrumbsArray($item); 221 | 222 | if (null === $subItem) { 223 | return $breadcrumbs; 224 | } 225 | 226 | if ($subItem instanceof ItemInterface) { 227 | $breadcrumbs[] = $this->getBreadcrumbsItem($subItem); 228 | 229 | return $breadcrumbs; 230 | } 231 | 232 | if (!\is_array($subItem) && !$subItem instanceof \Traversable) { 233 | $subItem = [$subItem]; 234 | } 235 | 236 | foreach ($subItem as $key => $value) { 237 | switch (true) { 238 | case $value instanceof ItemInterface: 239 | $value = $this->getBreadcrumbsItem($value); 240 | break; 241 | 242 | case \is_array($value): 243 | // Assume we already have the appropriate array format for the element 244 | break; 245 | 246 | case \is_int($key) && \is_string($value): 247 | $value = [ 248 | 'label' => $value, 249 | 'uri' => null, 250 | 'item' => null, 251 | ]; 252 | break; 253 | 254 | case \is_scalar($value): 255 | $value = [ 256 | 'label' => (string) $key, 257 | 'uri' => (string) $value, 258 | 'item' => null, 259 | ]; 260 | break; 261 | 262 | case null === $value: 263 | $value = [ 264 | 'label' => (string) $key, 265 | 'uri' => null, 266 | 'item' => null, 267 | ]; 268 | break; 269 | 270 | default: 271 | throw new \InvalidArgumentException(\sprintf('Invalid value supplied for the key "%s". It should be an item, an array or a scalar', $key)); 272 | } 273 | 274 | $breadcrumbs[] = $value; 275 | } 276 | 277 | return $breadcrumbs; 278 | } 279 | 280 | /** 281 | * @phpstan-return list 282 | */ 283 | private function buildBreadcrumbsArray(ItemInterface $item): array 284 | { 285 | $breadcrumb = []; 286 | 287 | do { 288 | $breadcrumb[] = $this->getBreadcrumbsItem($item); 289 | } while ($item = $item->getParent()); 290 | 291 | return \array_reverse($breadcrumb); 292 | } 293 | 294 | /** 295 | * @phpstan-return array{label: string, uri: string|null, item: ItemInterface} 296 | */ 297 | private function getBreadcrumbsItem(ItemInterface $item): array 298 | { 299 | return [ 300 | 'label' => $item->getLabel(), 301 | 'uri' => $item->getUri(), 302 | 'item' => $item, 303 | ]; 304 | } 305 | } 306 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Page Toc Plugin (Anchors + Table of Contents) 2 | 3 | The **Page Toc** Plugin is for [Grav CMS](http://github.com/getgrav/grav) that generates anchors based on HTML header tags, and can also create a table of contents from those headers. 4 | 5 | With version `3.0` this plugin is able to automatically generate anchor links with hover-click capability without the need for other plugins (such as the `anchors` plugin). This functionality operates independently from the now optional "table of contents" functionality. 6 | 7 | ![](assets/page-toc.png) 8 | 9 | ## Installation 10 | 11 | Installing the Page Toc plugin can be done in one of two ways. The GPM (Grav Package Manager) installation method enables you to quickly and easily install the plugin with a simple terminal command, while the manual method enables you to do so via a zip file. 12 | 13 | ### GPM Installation (Preferred) 14 | 15 | The simplest way to install this plugin is via the [Grav Package Manager (GPM)](http://learn.getgrav.org/advanced/grav-gpm) through your system's terminal (also called the command line). From the root of your Grav install type: 16 | 17 | bin/gpm install page-toc 18 | 19 | This will install the Page Toc plugin into your `/user/plugins` directory within Grav. Its files can be found under `/your/site/grav/user/plugins/page-toc`. 20 | 21 | ### Manual Installation 22 | 23 | To install this plugin, just download the zip version of this repository and unzip it under `/your/site/grav/user/plugins`. Then, rename the folder to `page-toc`. You can find these files on [GitHub](https://github.com/team-grav/grav-plugin-page-toc) or via [GetGrav.org](http://getgrav.org/downloads/plugins#extras). 24 | 25 | You should now have all the plugin files under 26 | 27 | /your/site/grav/user/plugins/page-toc 28 | 29 | ## Configuration 30 | 31 | Before configuring this plugin, you should copy the `user/plugins/page-toc/page-toc.yaml` to `user/config/plugins/page-toc.yaml` and only edit that copy. 32 | 33 | Here is the default configuration and an explanation of available options: 34 | 35 | ```yaml 36 | enabled: true # Plugin enabled 37 | include_css: true # Include CSS 38 | active: true # Anchor IDs processed and generated for all pages 39 | templates: # Templates for which anchors should be generated if default is disabled 40 | start: 1 # Start header tag level (1 = h1) for TOC 41 | depth: 6 # Depth from start (2 = 2 levels deep) for TOC 42 | hclass: # Custom Header TOC styling classes 43 | anchors: # Anchor configuration 44 | start: 1 # Start header tag level (1 = h1) 45 | depth: 6 # Depth from start (2 = 2 levels deep) 46 | link: true # Enabled auto-generation of clickable link with fragment 47 | aria: Anchor # Aria label to use 48 | class: # Custom Header anchor styling classes 49 | icon: '#' # Icon to use, can be a symbol, emoji, ascii etc. 50 | position: after # Position to put the anchor, `before|after` 51 | copy_to_clipboard: true # Copy to clipboard functionality (coming soon) 52 | slug_maxlen: 25 # Max length of slugs used for anchors 53 | slug_prefix: # A prefix used in front of generated slugs 54 | ``` 55 | 56 | > You can now have `page-toc` automatically add anchors without there being a table of contents being used, just ensure `active` to `true`. 57 | 58 | By default, The plugin is `active` and will add header id attributes anchors for each header level found in a page. You can set `active: false` and then activate on a page basis by adding this to the page frontmatter: 59 | 60 | ```yaml 61 | page-toc: 62 | active: true 63 | ``` 64 | 65 | Alternatively, you can activate anchor generation on all pages using a given set of `templates`. 66 | 67 | You can also configure which header tags to start and depth on when building the id attribute anchors by changing the `start` and `depth` values. This can also be done on a per-page basis. 68 | 69 | For example if you had a start of `3` and a depth of `3` you would get a TOC for `h3`, `h4`, and `h5`. 70 | 71 | ## Usage 72 | 73 | ### Shortcode-like syntax in your content 74 | 75 | You can use the following shortcode-like syntax in your content: 76 | 77 | ```md 78 | [TOC] or [TOC/] or [toc] or [toc /] 79 | ``` 80 | 81 | This will replace the shortcode syntax with the Table of Contents with the `components/page-toc.html.twig` Twig template. Either the default one included in the `page-toc` plugin or an overridden version from your theme. 82 | 83 | For example in Quark theme, you will need to create a folder called `components/` under `templates/` so the file will be copied to: 84 | 85 | ```shell 86 | user/themes/quark/templates/components/page-toc.html.twig 87 | ``` 88 | 89 | NOTE: It's not required to set the TOC plugin `active` if you use the shortcode syntax in your content. That is a good enough indication that you want the plugin to be active. 90 | 91 | ### Customizing specific anchors 92 | 93 | There are situations where you want to have absolute control over the exact anchor link rather than letting page-toc create one for you. The best way to achieve this is to add your own `id` attribute to the header tag. This can be done either via HTML in your markdown directly: 94 | 95 | ```html 96 |

    H2 Header

    97 | ``` 98 | 99 | Or via using the header shortcodes. This approach is particularly useful if you have markdown inside your header tag: 100 | 101 | ```markdown 102 | [h2 id="my-custom-anchor"]H2 _header_[/h2] 103 | ``` 104 | 105 | If an `id` is found in one of the header tags that page-toc is configured to use for anchors, then it will use the provided value for the anchor id. 106 | 107 | ### Anchor Shortcode 108 | 109 | Page TOC now includes a `anchor` shortcode that allows you to manually add linkable fragments in your content. 110 | The shortcode will automatically generate the link if no options are provided. Alternatively you can use the BBCode syntax of `anchor="some-custom-id"` or you can explicity set it. You can also set a `prefix` and let the shortcode autogenerate the rest. 111 | 112 | For example: 113 | 114 | ```markdown 115 | 116 | Ut sed nisl suscipit metus sollicitudin [anchor]ornare[/anchor] nec vitae nulla. In pretium massa ex, in [anchor="vulputate"]vulputate tellus[/anchor] accumsan vel. 117 | 118 | Nullam [anchor id="tempor"]tempor quis lorem[/anchor] venenatis finibus. Curabitur dapibus nulla sed tristique pretium. Nullam tempor quis [anchor prefix="sec2.2-"]lorem venenatis finibus[/anchor]. 119 | ``` 120 | 121 | An example of the resulting HTML link looks like: 122 | 123 | ```html 124 | tempor quis lorem 125 | ``` 126 | 127 | The `inline-anchor` CSS class is used by shortcodes and any manually generated elements so it can be styled as independently from other links or anchored headers. 128 | 129 | ### Twig Templating 130 | 131 | When the plugin is `active` it will add anchors to the header tags of the page content as configured. You can simply include the provided Twig template: 132 | 133 | ```twig 134 | {% block content %} 135 | {% include 'components/page-toc.html.twig' %} 136 | {{ content|raw }} 137 | {% endblock %} 138 | ``` 139 | 140 | You can also add your **Table of Contents** HTML in your Twig template directly with the provided `toc()` Twig function: 141 | 142 | For example: 143 | 144 | ```twig 145 | {% if active or toc_config_var('active') %} 146 |
    147 | {% set table_of_contents = toc(page.content) %} 148 | {% if table_of_contents is not empty %} 149 |

    {{ 'PLUGIN_PAGE_TOC.TABLE_OF_CONTENTS'|t }}

    150 | {{ table_of_contents|raw }} 151 | {% endif %} 152 |
    153 | {% endif %} 154 | ``` 155 | 156 | The `toc_ordered()` Twig function does the same things as a the `toc()` function, except it uses an ordered list instead of an unordered one. 157 | 158 | or via the `toc_items()` function which rather than returning HTML directly returns objects and you can manipulate the output as needed: 159 | 160 | ```twig 161 | {% macro toc_loop(items) %} 162 | {% import _self as self %} 163 | {% for item in items %} 164 | {% set class = loop.first ? 'first' : loop.last ? 'last' : null %} 165 |
  • 166 | {{ item.label }} 167 | {% if item.children|length > 0 %} 168 |
      169 | {{ _self.toc_loop(item.children) }} 170 |
    171 | {% endif %} 172 |
  • 173 | {% endfor %} 174 | {% endmacro %} 175 | 176 | {% if config.get('plugins.page-toc.active') or attribute(page.header, 'page-toc').active %} 177 |
    178 | {% set table_of_contents = toc_items(page.content) %} 179 | {% if table_of_contents is not empty %} 180 |

    Table of Contents

    181 |
      182 | {{ _self.toc_loop(table_of_contents.children) }} 183 |
    184 | {% endif %} 185 |
    186 | {% endif %} 187 | ``` 188 | 189 | To explictly build a table of contents for a block of content: 190 | 191 | ```markdown 192 | {% block my_content %} 193 | # Header 1 194 | 195 | ## Header 1.1 196 | 197 | Nullam tempor quis lorem venenatis finibus. Maecenas ut condimentum nibh. Ut sed nisl suscipit metus sollicitudin ornare nec vitae nulla. Integer sed tortor eu ligula interdum rhoncus. Sed pulvinar ut massa et ullamcorper. Curabitur bibendum ante orci, nec porttitor dolor suscipit quis. Nulla et eros enim. 198 | 199 | ### Header 1.1.1 200 | 201 | Integer sed tortor eu ligula interdum rhoncus. 202 | 203 | ## Header 1.2 204 | {% endblock %} 205 | 206 | #### Table O' Contents 207 | {{ toc(block('my_content'), 2, 1) }} 208 | ``` 209 | 210 | The `add_anchors()` twig funtion can take a string or a block of content and automatically adds anchors to any headers found per the configuration for the page, but you can override the start and depth. For example here we have a Twig block but we just want to add anchors to the H2 tags: 211 | 212 | ```markdown 213 | {% block my_content %} 214 | # Header 1 215 | 216 | ## Header 1.1 217 | 218 | Nullam tempor quis lorem venenatis finibus. Maecenas ut condimentum nibh. Ut sed nisl suscipit metus sollicitudin ornare nec vitae nulla. Integer sed tortor eu ligula interdum rhoncus. Sed pulvinar ut massa et ullamcorper. Curabitur bibendum ante orci, nec porttitor dolor suscipit quis. Nulla et eros enim. 219 | 220 | ### Header 1.1.1 221 | 222 | Integer sed tortor eu ligula interdum rhoncus. 223 | 224 | ## Header 1.2 225 | {% endblock %} 226 | 227 | #### Anchors Away! 228 | {{ add_anchors(block('my_content'), 2, 1) }} 229 | ``` 230 | 231 | ### Limiting levels in output 232 | 233 | As well as limiting the levels that the page TOC plugin will use in the table of contents, you can also limit the levels that are actually displayed. To do this you can pass an optional `start`, and `depth` value to the `toc()`, `toc_ordered()` , `toc_items()` and `add_anchors()` Twig functions: 234 | 235 | ```twig 236 | {% set table_of_contents = toc(page.content, 3, 3) %} 237 | ``` 238 | 239 | This will only display `H3` , and **3** levels deeper (up to `H5`) in the TOC output. 240 | 241 | ## Credits 242 | 243 | The majority of this plugin's functionality is provided by the [PHP TOC Generator](https://github.com/caseyamcl/toc) library by [Casey McLaughlin](https://github.com/caseyamcl). So Thanks for making this plugin for Grav possible! 244 | 245 | 246 | --------------------------------------------------------------------------------