$config Recipe config. */ public function requirements_met( array $config ): bool { return true; } /** * @param string $body_html Source HTML. * @param array $config Recipe config (attachments map optional). */ public function transform( string $body_html, array $config ): string { $attachments = isset( $config['attachments'] ) && is_array( $config['attachments'] ) ? $config['attachments'] : array(); if ( empty( $attachments ) ) { return $body_html; } $body_html = self::normalize_loose_markers( $body_html ); $builder = new Block_Markup_Builder(); $replace_token = static function ( array $matches ) use ( $attachments, $builder ): string { $block = self::block_for_token( (string) $matches[1], $attachments, $builder ); return null !== $block ? $block : $matches[0]; }; // Gutenberg paragraph that is only a pin marker — replace the whole block. $body_html = (string) preg_replace_callback( '/\s*]*>\s*(?:]*>)?\s*\[image:\s*([^\]]+?)\]\s*(?:<\/span>)?\s*<\/p>\s*/iu', $replace_token, $body_html ); // Whole paragraph wrappers from Google Docs export / pin UI. $body_html = (string) preg_replace_callback( '/]*>\s*(?:]*>)?\s*\[image:\s*([^\]]+?)\]\s*(?:<\/span>)?\s*<\/p>/iu', $replace_token, $body_html ); // Remaining bare markers. $body_html = (string) preg_replace_callback( '/\[image:\s*([^\]]+?)\]/iu', $replace_token, $body_html ); // First pass used to leave around the image. $body_html = (string) preg_replace( '/\s*()\s*/iu', '$1', $body_html ); $body_html = (string) preg_replace( '/\s*]*)?>(?:\s| |)*<\/p>\s*/iu', '', $body_html ); return trim( (string) preg_replace( "/\n{3,}/", "\n\n", $body_html ) ); } /** * Collect unique filenames referenced by [image:…] markers. * * @return array */ public static function extract_tokens( string $body_html ): array { if ( ! preg_match_all( '/\[image:\s*([^\]]+?)\]/iu', $body_html, $matches ) ) { return array(); } $tokens = array(); foreach ( $matches[1] as $raw ) { $token = self::normalize_token( (string) $raw ); if ( '' !== $token ) { $tokens[ $token ] = $token; } } return array_values( $tokens ); } /** * `[file.png]` / `[file.png left]` from preview mistakes → `[image:file.png left]`. */ public static function normalize_loose_markers( string $html ): string { $out = (string) preg_replace_callback( '/\[(?!image:)(\s*[^\]\n]+\.(?:png|jpe?g|gif|webp|avif)(?:\s+(?:left|right|center))?)\s*\]/iu', static function ( array $matches ): string { $token = trim( (string) $matches[1] ); return '[image:' . $token . ']'; }, $html ); return is_string( $out ) ? $out : $html; } /** * @param string $token Marker value. * @param array $attachments Normalized map. * @param Block_Markup_Builder $builder Markup helper. */ private static function block_for_token( string $token, array $attachments, Block_Markup_Builder $builder ): ?string { $parsed = self::parse_marker( $token ); $key = $parsed['file']; if ( '' === $key || ! isset( $attachments[ $key ] ) || ! is_array( $attachments[ $key ] ) ) { return null; } $attachment = $attachments[ $key ]; $id = (int) ( $attachment['id'] ?? 0 ); $url = (string) ( $attachment['url'] ?? '' ); if ( $id <= 0 || '' === $url ) { return null; } $alt = isset( $attachment['alt'] ) ? (string) $attachment['alt'] : ''; return $builder->build_image_block( $id, $url, $alt, $parsed['align'] ); } /** * Split `[image:file.jpg left]` into filename + align. * * @return array{file: string, align: string} */ public static function parse_marker( string $token ): array { $token = html_entity_decode( $token, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); $token = trim( $token ); $token = trim( $token, " \t\n\r\0\x0B\"'" ); $align = ''; if ( preg_match( '/^(.*?)\s+(left|right|center)$/iu', $token, $m ) ) { $token = trim( $m[1] ); $align = strtolower( $m[2] ); } return array( 'file' => strtolower( $token ), 'align' => $align, ); } /** * @param string $token Raw marker contents. */ public static function normalize_token( string $token ): string { return self::parse_marker( $token )['file']; } }