| 1: | <?php |
| 2: | declare(strict_types=1); |
| 3: | |
| 4: | namespace om\ICal; |
| 5: | |
| 6: | use Countable; |
| 7: | use InvalidArgumentException; |
| 8: | use IteratorAggregate; |
| 9: | use Traversable; |
| 10: | |
| 11: | |
| 12: | |
| 13: | |
| 14: | |
| 15: | |
| 16: | |
| 17: | final class Parameters implements IteratorAggregate, Countable { |
| 18: | |
| 19: | |
| 20: | |
| 21: | private function __construct(private array $values = []) { |
| 22: | } |
| 23: | |
| 24: | |
| 25: | |
| 26: | |
| 27: | public static function from(array $parameters): self { |
| 28: | $values = []; |
| 29: | foreach ($parameters as $name => $value) { |
| 30: | $values[strtoupper(self::name($name))] = array_map('strval', (array) $value); |
| 31: | } |
| 32: | return new self($values); |
| 33: | } |
| 34: | |
| 35: | |
| 36: | |
| 37: | |
| 38: | |
| 39: | public static function parse(string $raw): self { |
| 40: | $values = []; |
| 41: | $length = strlen($raw); |
| 42: | $name = ''; |
| 43: | $value = ''; |
| 44: | $list = []; |
| 45: | $inName = true; |
| 46: | $quoted = false; |
| 47: | for ($i = 0; $i <= $length; $i++) { |
| 48: | $char = $i < $length ? $raw[$i] : ';'; |
| 49: | if ($quoted) { |
| 50: | if ($char === '"') { |
| 51: | $quoted = false; |
| 52: | } else { |
| 53: | $value .= $char; |
| 54: | } |
| 55: | continue; |
| 56: | } |
| 57: | if ($char === ';') { |
| 58: | if (!$inName && trim($name) !== '') { |
| 59: | $list[] = $value; |
| 60: | $values[strtoupper(trim($name))] = array_map(self::decode(...), $list); |
| 61: | } |
| 62: | [$name, $value, $list, $inName] = ['', '', [], true]; |
| 63: | } elseif ($inName) { |
| 64: | if ($char === '=') { |
| 65: | $inName = false; |
| 66: | } else { |
| 67: | $name .= $char; |
| 68: | } |
| 69: | } elseif ($char === '"') { |
| 70: | $quoted = true; |
| 71: | } elseif ($char === ',') { |
| 72: | $list[] = $value; |
| 73: | $value = ''; |
| 74: | } else { |
| 75: | $value .= $char; |
| 76: | } |
| 77: | } |
| 78: | return new self($values); |
| 79: | } |
| 80: | |
| 81: | |
| 82: | |
| 83: | |
| 84: | public function get(string $name): ?string { |
| 85: | $values = $this->values[strtoupper($name)] ?? null; |
| 86: | return $values === null ? null : implode(',', $values); |
| 87: | } |
| 88: | |
| 89: | |
| 90: | |
| 91: | |
| 92: | public function values(string $name): array { |
| 93: | return $this->values[strtoupper($name)] ?? []; |
| 94: | } |
| 95: | |
| 96: | public function has(string $name): bool { |
| 97: | return isset($this->values[strtoupper($name)]); |
| 98: | } |
| 99: | |
| 100: | |
| 101: | |
| 102: | |
| 103: | public function with(string $name, string|array|null $value): self { |
| 104: | $values = $this->values; |
| 105: | if ($value === null) { |
| 106: | unset($values[strtoupper($name)]); |
| 107: | } else { |
| 108: | $values[strtoupper(self::name($name))] = (array) $value; |
| 109: | } |
| 110: | return new self($values); |
| 111: | } |
| 112: | |
| 113: | |
| 114: | |
| 115: | |
| 116: | public function all(): array { |
| 117: | return $this->values; |
| 118: | } |
| 119: | |
| 120: | public function count(): int { |
| 121: | return count($this->values); |
| 122: | } |
| 123: | |
| 124: | public function getIterator(): Traversable { |
| 125: | yield from $this->values; |
| 126: | } |
| 127: | |
| 128: | |
| 129: | |
| 130: | |
| 131: | |
| 132: | public function __toString(): string { |
| 133: | $parts = []; |
| 134: | foreach ($this->values as $name => $values) { |
| 135: | $parts[] = $name . '=' . implode(',', array_map(self::encode(...), $values)); |
| 136: | } |
| 137: | return implode(';', $parts); |
| 138: | } |
| 139: | |
| 140: | private static function encode(string $value): string { |
| 141: | $value = strtr($value, ['^' => '^^', "\r\n" => '^n', "\n" => '^n', "\r" => '^n', '"' => "^'"]); |
| 142: | $value = (string) preg_replace('/[\x00-\x08\x0A-\x1F\x7F]/', '', $value); |
| 143: | return strpbrk($value, ':;,') === false ? $value : '"' . $value . '"'; |
| 144: | } |
| 145: | |
| 146: | |
| 147: | |
| 148: | |
| 149: | private static function decode(string $value): string { |
| 150: | return str_contains($value, '^') ? strtr($value, ['^^' => '^', '^n' => "\n", '^N' => "\n", "^'" => '"']) : $value; |
| 151: | } |
| 152: | |
| 153: | private static function name(string $name): string { |
| 154: | if (!preg_match('/^[A-Za-z0-9-]+$/D', $name)) { |
| 155: | throw new InvalidArgumentException("Invalid parameter name: $name"); |
| 156: | } |
| 157: | return $name; |
| 158: | } |
| 159: | } |
| 160: | |