, in text nodes only (never inside tags). */ declare( strict_types = 1 ); namespace AddBlack\Lang\MachineTranslation; defined( 'ABSPATH' ) || exit; final class Protector { private const OPEN = ''; private const CLOSE = ''; /** * Placeholders that are never translated, in order: Yoast variables, * Rank Math and Slim SEO variables, double-brace and single-brace tokens, * printf placeholders, e-mail addresses, @handles. */ private const PATTERNS = array( '%%[A-Za-z0-9_]+%%', '%[A-Za-z][A-Za-z0-9_]*%', '\{\{[^{}]+\}\}', '\{[A-Za-z0-9_]+\}', '%(?:\d+\$)?[sd]', '[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}', '@[A-Za-z0-9_.]+', ); /** @param string[] $glossary */ public function protect( string $html, array $glossary ): string { $terms = array_map( static fn( string $t ): string => preg_quote( $t, '/' ), $glossary ); $words = '(?]*>)/', $html, -1, PREG_SPLIT_DELIM_CAPTURE ); foreach ( $parts as $i => $part ) { if ( '' === $part || '<' === $part[0] ) { continue; } $parts[ $i ] = (string) preg_replace( $re, self::OPEN . '$1' . self::CLOSE, (string) $part ); } return implode( '', $parts ); } public function unprotect( string $html ): string { return (string) preg_replace( '#' . preg_quote( self::OPEN, '#' ) . '(.*?)' . preg_quote( self::CLOSE, '#' ) . '#su', '$1', $html ); } }