| 1: | <?php |
| 2: | declare(strict_types=1); |
| 3: | |
| 4: | namespace om\ICal; |
| 5: | |
| 6: | use Stringable; |
| 7: | |
| 8: | |
| 9: | |
| 10: | |
| 11: | |
| 12: | |
| 13: | |
| 14: | |
| 15: | |
| 16: | |
| 17: | |
| 18: | |
| 19: | |
| 20: | final class ContentLine implements Stringable { |
| 21: | private const string NAME_CHARACTERS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; |
| 22: | |
| 23: | |
| 24: | public Parameters $parameters { |
| 25: | get => $this->parsed ??= Parameters::parse($this->rawParameters); |
| 26: | } |
| 27: | |
| 28: | private ?Parameters $parsed = null; |
| 29: | |
| 30: | |
| 31: | |
| 32: | |
| 33: | |
| 34: | |
| 35: | |
| 36: | public function __construct( |
| 37: | public readonly string $name, |
| 38: | public readonly string $rawParameters, |
| 39: | public readonly string $value, |
| 40: | public readonly int $line = 0, |
| 41: | ) { |
| 42: | } |
| 43: | |
| 44: | |
| 45: | |
| 46: | |
| 47: | public static function parse(string $line, int $number = 0): ?self { |
| 48: | $parts = self::split($line); |
| 49: | return $parts === null ? null : new self($parts[0], $parts[1], $parts[2], $number); |
| 50: | } |
| 51: | |
| 52: | public function isBegin(): bool { |
| 53: | return $this->name === 'BEGIN' && $this->rawParameters === ''; |
| 54: | } |
| 55: | |
| 56: | public function isEnd(): bool { |
| 57: | return $this->name === 'END' && $this->rawParameters === ''; |
| 58: | } |
| 59: | |
| 60: | |
| 61: | |
| 62: | |
| 63: | public function componentName(): string { |
| 64: | return strtoupper(trim($this->value)); |
| 65: | } |
| 66: | |
| 67: | public function __toString(): string { |
| 68: | return $this->name . ($this->rawParameters === '' ? '' : ';' . $this->rawParameters) . ':' . $this->value; |
| 69: | } |
| 70: | |
| 71: | |
| 72: | |
| 73: | |
| 74: | |
| 75: | |
| 76: | |
| 77: | public static function split(string $line): ?array { |
| 78: | $nameLength = strcspn($line, ';:'); |
| 79: | if ($nameLength === 0 || $nameLength === strlen($line) || strspn($line, self::NAME_CHARACTERS, 0, $nameLength) !== $nameLength) { |
| 80: | return null; |
| 81: | } |
| 82: | |
| 83: | $parameters = ''; |
| 84: | $valueStart = $nameLength + 1; |
| 85: | if ($line[$nameLength] === ';') { |
| 86: | $colon = self::valueSeparator($line, $nameLength); |
| 87: | if ($colon === null) { |
| 88: | return null; |
| 89: | } |
| 90: | $parameters = substr($line, $nameLength + 1, $colon - $nameLength - 1); |
| 91: | $valueStart = $colon + 1; |
| 92: | } |
| 93: | return [strtoupper(substr($line, 0, $nameLength)), $parameters, substr($line, $valueStart)]; |
| 94: | } |
| 95: | |
| 96: | |
| 97: | |
| 98: | |
| 99: | |
| 100: | |
| 101: | |
| 102: | public static function parameters(string $parameters): array { |
| 103: | $result = []; |
| 104: | foreach (Parameters::parse($parameters) as $name => $values) { |
| 105: | $result[$name] = implode(',', $values); |
| 106: | } |
| 107: | return $result; |
| 108: | } |
| 109: | |
| 110: | |
| 111: | |
| 112: | |
| 113: | |
| 114: | private static function valueSeparator(string $line, int $offset): ?int { |
| 115: | $colon = strpos($line, ':', $offset); |
| 116: | if ($colon === false) { |
| 117: | return null; |
| 118: | } |
| 119: | $quote = strpos($line, '"', $offset); |
| 120: | if ($quote === false || $quote > $colon) { |
| 121: | return $colon; |
| 122: | } |
| 123: | $length = strlen($line); |
| 124: | $quoted = false; |
| 125: | for ($i = $quote; $i < $length; $i++) { |
| 126: | if ($line[$i] === '"') { |
| 127: | $quoted = !$quoted; |
| 128: | } elseif ($line[$i] === ':' && !$quoted) { |
| 129: | return $i; |
| 130: | } |
| 131: | } |
| 132: | return null; |
| 133: | } |
| 134: | } |
| 135: | |