1<?php 2 3/* 4 * This file is part of the league/commonmark package. 5 * 6 * (c) Colin O'Dell <colinodell@gmail.com> 7 * (c) 2015 Martin Hasoň <martin.hason@gmail.com> 8 * 9 * For the full copyright and license information, please view the LICENSE 10 * file that was distributed with this source code. 11 */ 12 13declare(strict_types=1); 14 15namespace League\CommonMark\Extension\Attributes\Util; 16 17use League\CommonMark\Node\Node; 18use League\CommonMark\Parser\Cursor; 19use League\CommonMark\Util\RegexHelper; 20 21/** 22 * @internal 23 */ 24final class AttributesHelper 25{ 26 private const SINGLE_ATTRIBUTE = '\s*([.#][_a-z0-9-]+|' . RegexHelper::PARTIAL_ATTRIBUTENAME . RegexHelper::PARTIAL_ATTRIBUTEVALUESPEC . ')\s*'; 27 private const ATTRIBUTE_LIST = '/^{:?(' . self::SINGLE_ATTRIBUTE . ')+}/i'; 28 29 /** 30 * @return array<string, mixed> 31 */ 32 public static function parseAttributes(Cursor $cursor): array 33 { 34 $state = $cursor->saveState(); 35 $cursor->advanceToNextNonSpaceOrNewline(); 36 37 // Quick check to see if we might have attributes 38 if ($cursor->getCharacter() !== '{') { 39 $cursor->restoreState($state); 40 41 return []; 42 } 43 44 // Attempt to match the entire attribute list expression 45 // While this is less performant than checking for '{' now and '}' later, it simplifies 46 // matching individual attributes since they won't need to look ahead for the closing '}' 47 // while dealing with the fact that attributes can technically contain curly braces. 48 // So we'll just match the start and end braces up front. 49 $attributeExpression = $cursor->match(self::ATTRIBUTE_LIST); 50 if ($attributeExpression === null) { 51 $cursor->restoreState($state); 52 53 return []; 54 } 55 56 // Trim the leading '{' or '{:' and the trailing '}' 57 $attributeExpression = \ltrim(\substr($attributeExpression, 1, -1), ':'); 58 $attributeCursor = new Cursor($attributeExpression); 59 60 /** @var array<string, mixed> $attributes */ 61 $attributes = []; 62 while ($attribute = \trim((string) $attributeCursor->match('/^' . self::SINGLE_ATTRIBUTE . '/i'))) { 63 if ($attribute[0] === '#') { 64 $attributes['id'] = \substr($attribute, 1); 65 66 continue; 67 } 68 69 if ($attribute[0] === '.') { 70 $attributes['class'][] = \substr($attribute, 1); 71 72 continue; 73 } 74 75 /** @psalm-suppress PossiblyUndefinedArrayOffset */ 76 [$name, $value] = \explode('=', $attribute, 2); 77 78 $first = $value[0]; 79 $last = \substr($value, -1); 80 if (($first === '"' && $last === '"') || ($first === "'" && $last === "'") && \strlen($value) > 1) { 81 $value = \substr($value, 1, -1); 82 } 83 84 if (\strtolower(\trim($name)) === 'class') { 85 foreach (\array_filter(\explode(' ', \trim($value))) as $class) { 86 $attributes['class'][] = $class; 87 } 88 } else { 89 $attributes[\trim($name)] = \trim($value); 90 } 91 } 92 93 if (isset($attributes['class'])) { 94 $attributes['class'] = \implode(' ', (array) $attributes['class']); 95 } 96 97 return $attributes; 98 } 99 100 /** 101 * @param Node|array<string, mixed> $attributes1 102 * @param Node|array<string, mixed> $attributes2 103 * 104 * @return array<string, mixed> 105 */ 106 public static function mergeAttributes($attributes1, $attributes2): array 107 { 108 $attributes = []; 109 foreach ([$attributes1, $attributes2] as $arg) { 110 if ($arg instanceof Node) { 111 $arg = $arg->data->get('attributes'); 112 } 113 114 /** @var array<string, mixed> $arg */ 115 $arg = (array) $arg; 116 if (isset($arg['class'])) { 117 if (\is_string($arg['class'])) { 118 $arg['class'] = \array_filter(\explode(' ', \trim($arg['class']))); 119 } 120 121 foreach ($arg['class'] as $class) { 122 $attributes['class'][] = $class; 123 } 124 125 unset($arg['class']); 126 } 127 128 $attributes = \array_merge($attributes, $arg); 129 } 130 131 if (isset($attributes['class'])) { 132 $attributes['class'] = \implode(' ', $attributes['class']); 133 } 134 135 return $attributes; 136 } 137} 138