## Major CodePress CMS Enhancement
### 🚀 New Features - **League CommonMark Integration**: Replaced basic Markdown parser with full CommonMark 2.7 support - **Bootstrap Sass Architecture**: Modern SCSS build system with Bootstrap 5.3.8 - **Enhanced Navigation**: Uses filenames instead of H1 titles for consistency - **Improved Styling**: Transparent navigation backgrounds, no rounded corners ### 🎨 UI/UX Improvements - Navigation items now use formatted filenames (e.g., "kennis-boven-aantallen" → "Kennis Boven Aantallen") - Transparent navigation backgrounds with subtle hover effects - Removed rounded corners from first-level navigation - 50% opacity navigation background using Bootstrap variables ### 🔧 Technical Improvements - **Class Organization**: Extracted CodePressCMS and SimpleTemplate to separate files - **Full PHPDoc Documentation**: Complete documentation for all methods - **Modern Build Process**: npm scripts for SCSS compilation - **Enhanced Markdown Support**: Tables, strikethrough, task lists, autolinks - **Security**: Proper HTML sanitization with CommonMark ### 📦 Dependencies - Added `league/commonmark` for professional Markdown parsing - Added `bootstrap` for SCSS-based styling - Updated `sass` build process ### 🐛 Bug Fixes - Fixed content directory path configuration - Resolved navigation title inconsistencies - Improved Markdown bold/italic formatting - Fixed homepage 404 issues ### 🔄 Migration Notes - Content directory moved from `content/` to `public/content/` - Navigation now displays filenames instead of content H1 titles - CSS now compiled from SCSS source files The CMS now provides a professional, modern experience with robust Markdown support and clean, maintainable code architecture.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Attributes;
|
||||
|
||||
use League\CommonMark\Environment\EnvironmentBuilderInterface;
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Extension\Attributes\Event\AttributesListener;
|
||||
use League\CommonMark\Extension\Attributes\Parser\AttributesBlockStartParser;
|
||||
use League\CommonMark\Extension\Attributes\Parser\AttributesInlineParser;
|
||||
use League\CommonMark\Extension\ConfigurableExtensionInterface;
|
||||
use League\Config\ConfigurationBuilderInterface;
|
||||
use Nette\Schema\Expect;
|
||||
|
||||
final class AttributesExtension implements ConfigurableExtensionInterface
|
||||
{
|
||||
public function configureSchema(ConfigurationBuilderInterface $builder): void
|
||||
{
|
||||
$builder->addSchema('attributes', Expect::structure([
|
||||
'allow' => Expect::arrayOf('string')->default([]),
|
||||
]));
|
||||
}
|
||||
|
||||
public function register(EnvironmentBuilderInterface $environment): void
|
||||
{
|
||||
$allowList = $environment->getConfiguration()->get('attributes.allow');
|
||||
$allowUnsafeLinks = $environment->getConfiguration()->get('allow_unsafe_links');
|
||||
|
||||
$environment->addBlockStartParser(new AttributesBlockStartParser());
|
||||
$environment->addInlineParser(new AttributesInlineParser());
|
||||
$environment->addEventListener(DocumentParsedEvent::class, [new AttributesListener($allowList, $allowUnsafeLinks), 'processDocument']);
|
||||
}
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Attributes\Event;
|
||||
|
||||
use League\CommonMark\Event\DocumentParsedEvent;
|
||||
use League\CommonMark\Extension\Attributes\Node\Attributes;
|
||||
use League\CommonMark\Extension\Attributes\Node\AttributesInline;
|
||||
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock;
|
||||
use League\CommonMark\Extension\CommonMark\Node\Block\ListItem;
|
||||
use League\CommonMark\Node\Inline\AbstractInline;
|
||||
use League\CommonMark\Node\Node;
|
||||
|
||||
final class AttributesListener
|
||||
{
|
||||
private const DIRECTION_PREFIX = 'prefix';
|
||||
private const DIRECTION_SUFFIX = 'suffix';
|
||||
|
||||
/** @var list<string> */
|
||||
private array $allowList;
|
||||
private bool $allowUnsafeLinks;
|
||||
|
||||
/**
|
||||
* @param list<string> $allowList
|
||||
*/
|
||||
public function __construct(array $allowList = [], bool $allowUnsafeLinks = true)
|
||||
{
|
||||
$this->allowList = $allowList;
|
||||
$this->allowUnsafeLinks = $allowUnsafeLinks;
|
||||
}
|
||||
|
||||
public function processDocument(DocumentParsedEvent $event): void
|
||||
{
|
||||
foreach ($event->getDocument()->iterator() as $node) {
|
||||
if (! ($node instanceof Attributes || $node instanceof AttributesInline)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$target, $direction] = self::findTargetAndDirection($node);
|
||||
|
||||
if ($target instanceof Node) {
|
||||
$parent = $target->parent();
|
||||
if ($parent instanceof ListItem && $parent->parent() instanceof ListBlock && $parent->parent()->isTight()) {
|
||||
$target = $parent;
|
||||
}
|
||||
|
||||
if ($direction === self::DIRECTION_SUFFIX) {
|
||||
$attributes = AttributesHelper::mergeAttributes($target, $node->getAttributes());
|
||||
} else {
|
||||
$attributes = AttributesHelper::mergeAttributes($node->getAttributes(), $target);
|
||||
}
|
||||
|
||||
$target->data->set('attributes', AttributesHelper::filterAttributes($attributes, $this->allowList, $this->allowUnsafeLinks));
|
||||
}
|
||||
|
||||
$node->detach();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Attributes|AttributesInline $node
|
||||
*
|
||||
* @return array<Node|string|null>
|
||||
*/
|
||||
private static function findTargetAndDirection($node): array
|
||||
{
|
||||
$target = null;
|
||||
$direction = null;
|
||||
$previous = $next = $node;
|
||||
while (true) {
|
||||
$previous = self::getPrevious($previous);
|
||||
$next = self::getNext($next);
|
||||
|
||||
if ($previous === null && $next === null) {
|
||||
if (! $node->parent() instanceof FencedCode) {
|
||||
$target = $node->parent();
|
||||
$direction = self::DIRECTION_SUFFIX;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($node instanceof AttributesInline && ($previous === null || ($previous instanceof AbstractInline && $node->isBlock()))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($previous !== null && ! self::isAttributesNode($previous)) {
|
||||
$target = $previous;
|
||||
$direction = self::DIRECTION_SUFFIX;
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if ($next !== null && ! self::isAttributesNode($next)) {
|
||||
$target = $next;
|
||||
$direction = self::DIRECTION_PREFIX;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return [$target, $direction];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any previous block (sibling or parent) this might apply to
|
||||
*/
|
||||
private static function getPrevious(?Node $node = null): ?Node
|
||||
{
|
||||
if ($node instanceof Attributes) {
|
||||
if ($node->getTarget() === Attributes::TARGET_NEXT) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($node->getTarget() === Attributes::TARGET_PARENT) {
|
||||
return $node->parent();
|
||||
}
|
||||
}
|
||||
|
||||
return $node instanceof Node ? $node->previous() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get any previous block (sibling or parent) this might apply to
|
||||
*/
|
||||
private static function getNext(?Node $node = null): ?Node
|
||||
{
|
||||
if ($node instanceof Attributes && $node->getTarget() !== Attributes::TARGET_NEXT) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $node instanceof Node ? $node->next() : null;
|
||||
}
|
||||
|
||||
private static function isAttributesNode(Node $node): bool
|
||||
{
|
||||
return $node instanceof Attributes || $node instanceof AttributesInline;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Attributes\Node;
|
||||
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
|
||||
final class Attributes extends AbstractBlock
|
||||
{
|
||||
public const TARGET_PARENT = 0;
|
||||
public const TARGET_PREVIOUS = 1;
|
||||
public const TARGET_NEXT = 2;
|
||||
|
||||
/** @var array<string, mixed> */
|
||||
private array $attributes;
|
||||
|
||||
private int $target = self::TARGET_NEXT;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function __construct(array $attributes)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function setAttributes(array $attributes): void
|
||||
{
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
public function getTarget(): int
|
||||
{
|
||||
return $this->target;
|
||||
}
|
||||
|
||||
public function setTarget(int $target): void
|
||||
{
|
||||
$this->target = $target;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Attributes\Node;
|
||||
|
||||
use League\CommonMark\Node\Inline\AbstractInline;
|
||||
|
||||
final class AttributesInline extends AbstractInline
|
||||
{
|
||||
/** @var array<string, mixed> */
|
||||
private array $attributes;
|
||||
|
||||
private bool $block;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function __construct(array $attributes, bool $block)
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->attributes = $attributes;
|
||||
$this->block = $block;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function getAttributes(): array
|
||||
{
|
||||
return $this->attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
*/
|
||||
public function setAttributes(array $attributes): void
|
||||
{
|
||||
$this->attributes = $attributes;
|
||||
}
|
||||
|
||||
public function isBlock(): bool
|
||||
{
|
||||
return $this->block;
|
||||
}
|
||||
}
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Attributes\Parser;
|
||||
|
||||
use League\CommonMark\Extension\Attributes\Node\Attributes;
|
||||
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
|
||||
use League\CommonMark\Node\Block\AbstractBlock;
|
||||
use League\CommonMark\Parser\Block\AbstractBlockContinueParser;
|
||||
use League\CommonMark\Parser\Block\BlockContinue;
|
||||
use League\CommonMark\Parser\Block\BlockContinueParserInterface;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
|
||||
final class AttributesBlockContinueParser extends AbstractBlockContinueParser
|
||||
{
|
||||
private Attributes $block;
|
||||
|
||||
private AbstractBlock $container;
|
||||
|
||||
private bool $hasSubsequentLine = false;
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes The attributes identified by the block start parser
|
||||
* @param AbstractBlock $container The node we were in when these attributes were discovered
|
||||
*/
|
||||
public function __construct(array $attributes, AbstractBlock $container)
|
||||
{
|
||||
$this->block = new Attributes($attributes);
|
||||
|
||||
$this->container = $container;
|
||||
}
|
||||
|
||||
public function getBlock(): AbstractBlock
|
||||
{
|
||||
return $this->block;
|
||||
}
|
||||
|
||||
public function tryContinue(Cursor $cursor, BlockContinueParserInterface $activeBlockParser): ?BlockContinue
|
||||
{
|
||||
$this->hasSubsequentLine = true;
|
||||
|
||||
$cursor->advanceToNextNonSpaceOrTab();
|
||||
|
||||
// Does this next line also have attributes?
|
||||
$attributes = AttributesHelper::parseAttributes($cursor);
|
||||
$cursor->advanceToNextNonSpaceOrTab();
|
||||
if ($cursor->isAtEnd() && $attributes !== []) {
|
||||
// It does! Merge them into what we parsed previously
|
||||
$this->block->setAttributes(AttributesHelper::mergeAttributes(
|
||||
$this->block->getAttributes(),
|
||||
$attributes
|
||||
));
|
||||
|
||||
// Tell the core parser we've consumed everything
|
||||
return BlockContinue::at($cursor);
|
||||
}
|
||||
|
||||
// Okay, so there are no attributes on the next line
|
||||
// If this next line is blank we know we can't target the next node, it must be a previous one
|
||||
if ($cursor->isBlank()) {
|
||||
$this->block->setTarget(Attributes::TARGET_PREVIOUS);
|
||||
}
|
||||
|
||||
return BlockContinue::none();
|
||||
}
|
||||
|
||||
public function closeBlock(): void
|
||||
{
|
||||
// Attributes appearing at the very end of the document won't have any last lines to check
|
||||
// so we can make that determination here
|
||||
if (! $this->hasSubsequentLine) {
|
||||
$this->block->setTarget(Attributes::TARGET_PREVIOUS);
|
||||
}
|
||||
|
||||
// We know this block must apply to the "previous" block, but that could be a sibling or parent,
|
||||
// so we check the containing block to see which one it might be.
|
||||
if ($this->block->getTarget() === Attributes::TARGET_PREVIOUS && $this->block->parent() === $this->container) {
|
||||
$this->block->setTarget(Attributes::TARGET_PARENT);
|
||||
}
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Attributes\Parser;
|
||||
|
||||
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
|
||||
use League\CommonMark\Parser\Block\BlockStart;
|
||||
use League\CommonMark\Parser\Block\BlockStartParserInterface;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Parser\MarkdownParserStateInterface;
|
||||
|
||||
final class AttributesBlockStartParser implements BlockStartParserInterface
|
||||
{
|
||||
public function tryStart(Cursor $cursor, MarkdownParserStateInterface $parserState): ?BlockStart
|
||||
{
|
||||
$originalPosition = $cursor->getPosition();
|
||||
$attributes = AttributesHelper::parseAttributes($cursor);
|
||||
|
||||
if ($attributes === [] && $originalPosition === $cursor->getPosition()) {
|
||||
return BlockStart::none();
|
||||
}
|
||||
|
||||
if ($cursor->getNextNonSpaceCharacter() !== null) {
|
||||
return BlockStart::none();
|
||||
}
|
||||
|
||||
return BlockStart::of(new AttributesBlockContinueParser($attributes, $parserState->getActiveBlockParser()->getBlock()))->at($cursor);
|
||||
}
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Attributes\Parser;
|
||||
|
||||
use League\CommonMark\Extension\Attributes\Node\AttributesInline;
|
||||
use League\CommonMark\Extension\Attributes\Util\AttributesHelper;
|
||||
use League\CommonMark\Node\StringContainerInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserInterface;
|
||||
use League\CommonMark\Parser\Inline\InlineParserMatch;
|
||||
use League\CommonMark\Parser\InlineParserContext;
|
||||
|
||||
final class AttributesInlineParser implements InlineParserInterface
|
||||
{
|
||||
public function getMatchDefinition(): InlineParserMatch
|
||||
{
|
||||
return InlineParserMatch::string('{');
|
||||
}
|
||||
|
||||
public function parse(InlineParserContext $inlineContext): bool
|
||||
{
|
||||
$cursor = $inlineContext->getCursor();
|
||||
$char = (string) $cursor->peek(-1);
|
||||
|
||||
$attributes = AttributesHelper::parseAttributes($cursor);
|
||||
if ($attributes === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($char === ' ' && ($prev = $inlineContext->getContainer()->lastChild()) instanceof StringContainerInterface) {
|
||||
$prev->setLiteral(\rtrim($prev->getLiteral(), ' '));
|
||||
}
|
||||
|
||||
if ($char === '') {
|
||||
$cursor->advanceToNextNonSpaceOrNewline();
|
||||
}
|
||||
|
||||
$node = new AttributesInline($attributes, $char === ' ' || $char === '');
|
||||
$inlineContext->getContainer()->appendChild($node);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the league/commonmark package.
|
||||
*
|
||||
* (c) Colin O'Dell <colinodell@gmail.com>
|
||||
* (c) 2015 Martin Hasoň <martin.hason@gmail.com>
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace League\CommonMark\Extension\Attributes\Util;
|
||||
|
||||
use League\CommonMark\Node\Node;
|
||||
use League\CommonMark\Parser\Cursor;
|
||||
use League\CommonMark\Util\RegexHelper;
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class AttributesHelper
|
||||
{
|
||||
private const SINGLE_ATTRIBUTE = '\s*([.]-?[_a-z][^\s.}]*|[#][^\s}]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . ')\s*';
|
||||
private const ATTRIBUTE_LIST = '/^{:?(' . self::SINGLE_ATTRIBUTE . ')+}/i';
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function parseAttributes(Cursor $cursor): array
|
||||
{
|
||||
$state = $cursor->saveState();
|
||||
$cursor->advanceToNextNonSpaceOrNewline();
|
||||
|
||||
// Quick check to see if we might have attributes
|
||||
if ($cursor->getCharacter() !== '{') {
|
||||
$cursor->restoreState($state);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Attempt to match the entire attribute list expression
|
||||
// While this is less performant than checking for '{' now and '}' later, it simplifies
|
||||
// matching individual attributes since they won't need to look ahead for the closing '}'
|
||||
// while dealing with the fact that attributes can technically contain curly braces.
|
||||
// So we'll just match the start and end braces up front.
|
||||
$attributeExpression = $cursor->match(self::ATTRIBUTE_LIST);
|
||||
if ($attributeExpression === null) {
|
||||
$cursor->restoreState($state);
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
// Trim the leading '{' or '{:' and the trailing '}'
|
||||
$attributeExpression = \ltrim(\substr($attributeExpression, 1, -1), ':');
|
||||
$attributeCursor = new Cursor($attributeExpression);
|
||||
|
||||
/** @var array<string, mixed> $attributes */
|
||||
$attributes = [];
|
||||
while ($attribute = \trim((string) $attributeCursor->match('/^' . self::SINGLE_ATTRIBUTE . '/i'))) {
|
||||
if ($attribute[0] === '#') {
|
||||
$attributes['id'] = \substr($attribute, 1);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($attribute[0] === '.') {
|
||||
$attributes['class'][] = \substr($attribute, 1);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
/** @psalm-suppress PossiblyUndefinedArrayOffset */
|
||||
[$name, $value] = \explode('=', $attribute, 2);
|
||||
|
||||
if ($value === 'true') {
|
||||
$attributes[$name] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
$first = $value[0];
|
||||
$last = \substr($value, -1);
|
||||
if (($first === '"' && $last === '"') || ($first === "'" && $last === "'") && \strlen($value) > 1) {
|
||||
$value = \substr($value, 1, -1);
|
||||
}
|
||||
|
||||
if (\strtolower(\trim($name)) === 'class') {
|
||||
foreach (\array_filter(\explode(' ', \trim($value))) as $class) {
|
||||
$attributes['class'][] = $class;
|
||||
}
|
||||
} else {
|
||||
$attributes[\trim($name)] = \trim($value);
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($attributes['class'])) {
|
||||
$attributes['class'] = \implode(' ', (array) $attributes['class']);
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Node|array<string, mixed> $attributes1
|
||||
* @param Node|array<string, mixed> $attributes2
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function mergeAttributes($attributes1, $attributes2): array
|
||||
{
|
||||
$attributes = [];
|
||||
foreach ([$attributes1, $attributes2] as $arg) {
|
||||
if ($arg instanceof Node) {
|
||||
$arg = $arg->data->get('attributes');
|
||||
}
|
||||
|
||||
/** @var array<string, mixed> $arg */
|
||||
$arg = (array) $arg;
|
||||
if (isset($arg['class'])) {
|
||||
if (\is_string($arg['class'])) {
|
||||
$arg['class'] = \array_filter(\explode(' ', \trim($arg['class'])));
|
||||
}
|
||||
|
||||
foreach ($arg['class'] as $class) {
|
||||
$attributes['class'][] = $class;
|
||||
}
|
||||
|
||||
unset($arg['class']);
|
||||
}
|
||||
|
||||
$attributes = \array_merge($attributes, $arg);
|
||||
}
|
||||
|
||||
if (isset($attributes['class'])) {
|
||||
$attributes['class'] = \implode(' ', $attributes['class']);
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $attributes
|
||||
* @param list<string> $allowList
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function filterAttributes(array $attributes, array $allowList, bool $allowUnsafeLinks): array
|
||||
{
|
||||
$allowList = \array_fill_keys($allowList, true);
|
||||
|
||||
foreach ($attributes as $name => $value) {
|
||||
$attrNameLower = \strtolower($name);
|
||||
|
||||
// Remove any unsafe links
|
||||
if (! $allowUnsafeLinks && ($attrNameLower === 'href' || $attrNameLower === 'src') && \is_string($value) && RegexHelper::isLinkPotentiallyUnsafe($value)) {
|
||||
unset($attributes[$name]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// No allowlist?
|
||||
if ($allowList === []) {
|
||||
// Just remove JS event handlers
|
||||
if (\str_starts_with($attrNameLower, 'on')) {
|
||||
unset($attributes[$name]);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Remove any attributes not in that allowlist (case-sensitive)
|
||||
if (! isset($allowList[$name])) {
|
||||
unset($attributes[$name]);
|
||||
}
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user