.*?)\1[^>]*>'; private const PATTERNS = array( 'link' => '#]*?\bhref' . self::ADDRESS_VALUE . '(?.*?)#is', 'image' => '#]*?\bsrc' . self::ADDRESS_VALUE . '#is', 'embed' => '#<(?:iframe|video|audio|source|embed)\s[^>]*?\bsrc' . self::ADDRESS_VALUE . '#is', ); private const ALT_PATTERN = '#\balt\s*=\s*(["\'])(?.*?)\1#is'; public function __construct( private AddressRules $address_rules, private PlainText $plain_text ) { } /** @return list */ public function find_in( string $content ): array { $found = array(); foreach ( self::PATTERNS as $kind => $pattern ) { $matches = array(); preg_match_all( $pattern, $content, $matches, PREG_SET_ORDER | PREG_OFFSET_CAPTURE ); foreach ( $matches as $match ) { $address = $this->address_rules->normalize( $match['address'][0] ); if ( null === $address ) { continue; } list( $tag, $tag_start ) = $match[0]; $found[] = new FoundAddress( $address, LinkKind::from( $kind ), $this->text_of( $tag, $match['text'][0] ?? '' ), $this->before( $content, $tag_start ), $this->after( $content, $tag_start + strlen( $tag ) ) ); } } return $found; } private function text_of( string $tag, string $inner_html ): string { if ( '' !== $inner_html ) { return trim( $this->plain_text->from_html( $inner_html ) ); } $alt = array(); preg_match( self::ALT_PATTERN, $tag, $alt ); return trim( $this->plain_text->from_html( $alt['alt'] ?? '' ) ); } private function before( string $content, int $offset ): string { $text = $this->plain_text->from_html( substr( $content, 0, $offset ) ); return mb_substr( $text, -self::CONTEXT_LENGTH ); } private function after( string $content, int $offset ): string { $text = $this->plain_text->from_html( substr( $content, $offset ) ); return mb_substr( $text, 0, self::CONTEXT_LENGTH ); } }