AttributesHelper.php 4.5 KB

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