classification_service = $classification_service;
}
/**
* Wraps existing image markup without rebuilding or changing its attributes.
*
* @param int $attachment_id Attachment ID.
* @param string $image_markup Existing img, picture, or linked-image markup.
* @param string|null $classification Optional preloaded user classification.
* @param array|null $decision Optional preloaded rule decision.
* @param array|null $settings Optional preloaded plugin settings.
* @return string
*/
public function render( $attachment_id, $image_markup, $classification = null, $decision = null, $settings = null ) {
$attachment_id = absint( $attachment_id );
$image_markup = (string) $image_markup;
if ( ! $attachment_id || '' === $image_markup || false !== strpos( $image_markup, 'aiparently-image' ) || false !== strpos( $image_markup, 'data-aiparently-processed' ) ) {
return $image_markup;
}
if ( 'attachment' !== get_post_type( $attachment_id ) ) {
return $image_markup;
}
$settings = is_array( $settings ) ? $settings : AIparently_Disclosure_Service::get_settings();
if ( empty( $settings['enabled'] ) || empty( $settings['images_enabled'] ) ) {
return $image_markup;
}
$classification = null === $classification
? $this->classification_service->get_status( $attachment_id )
: AIparently_Classification_Service::sanitize_status( $classification );
$decision = is_array( $decision )
? $decision
: $this->classification_service->get_decision( $attachment_id );
if ( ! in_array( $classification, array( 'assisted', 'generated' ), true ) || empty( $decision['required'] ) || empty( $decision['label'] ) ) {
return $image_markup;
}
$style = isset( $settings['style'] ) ? sanitize_html_class( $settings['style'] ) : 'badge';
$position = isset( $settings['image_label_position'] ) && in_array( $settings['image_label_position'], array( 'left', 'center', 'right' ), true )
? $settings['image_label_position']
: 'right';
$image_markup = $this->mark_first_image_processed( $image_markup );
return sprintf(
'%3$s%8$s',
$attachment_id,
esc_attr( $classification ),
$image_markup,
esc_attr( $style ),
esc_attr( $position ),
esc_attr( $decision['code'] ),
esc_attr( $decision['ruleset_version'] ),
esc_html( $decision['label'] )
);
}
/**
* Marks the original img so later WordPress content filters cannot wrap it
* a second time. All existing responsive, lazy-loading, style, and builder
* attributes remain untouched.
*/
private function mark_first_image_processed( $image_markup ) {
if ( false !== strpos( $image_markup, 'data-aiparently-processed' ) ) {
return $image_markup;
}
if ( class_exists( 'WP_HTML_Tag_Processor' ) ) {
$processor = new WP_HTML_Tag_Processor( $image_markup );
if ( $processor->next_tag( 'IMG' ) ) {
$processor->set_attribute( 'data-aiparently-processed', '1' );
return $processor->get_updated_html();
}
}
return preg_replace( '/
]*>\s*]*>.*?<\/picture>\s*<\/a>/is',
'/]*>\s*
]*>\s*<\/a>/is',
'/]*>.*?<\/picture>/is',
'/
]*>/is',
);
foreach ( $patterns as $pattern ) {
if ( preg_match( $pattern, $component_markup ) ) {
$renderer = $this;
return preg_replace_callback(
$pattern,
static function ( $matches ) use ( $renderer, $attachment_id ) {
return $renderer->render( $attachment_id, $matches[0] );
},
$component_markup,
1
);
}
}
return $component_markup;
}
/**
* Finds media-library images in one already-scoped block/widget and renders
* every classified image. This never receives or scans a complete page.
*/
public function render_detected_images( $component_markup, $known_attachment_id = 0 ) {
$component_markup = (string) $component_markup;
if ( '' === $component_markup || false !== strpos( $component_markup, 'aiparently-image' ) ) {
return $component_markup;
}
$renderer = $this;
$known_attachment_id = absint( $known_attachment_id );
$match_index = 0;
return preg_replace_callback(
'/]*>.*?<\/picture>|
]*>/is',
static function ( $matches ) use ( $renderer, $known_attachment_id, &$match_index ) {
$attachment_id = 0 === $match_index && $known_attachment_id
? $known_attachment_id
: $renderer->resolve_attachment_id( $matches[0] );
++$match_index;
return $attachment_id ? $renderer->render( $attachment_id, $matches[0] ) : $matches[0];
},
$component_markup
);
}
/**
* Resolves the attachment behind an img/picture without relying on a theme
* class. Explicit IDs win; media URLs are the compatible fallback.
*/
public function resolve_attachment_id( $image_markup ) {
$image_markup = (string) $image_markup;
$attachment_id = 0;
$class_name = '';
$src = '';
if ( class_exists( 'WP_HTML_Tag_Processor' ) ) {
$processor = new WP_HTML_Tag_Processor( $image_markup );
if ( $processor->next_tag( 'IMG' ) ) {
$attachment_id = absint( $processor->get_attribute( 'data-attachment-id' ) );
$class_name = (string) $processor->get_attribute( 'class' );
$src = (string) $processor->get_attribute( 'src' );
}
} else {
if ( preg_match( '/\bdata-attachment-id=["\'](\d+)["\']/i', $image_markup, $match ) ) {
$attachment_id = absint( $match[1] );
}
if ( preg_match( '/\bclass=["\']([^"\']*)["\']/i', $image_markup, $match ) ) {
$class_name = $match[1];
}
if ( preg_match( '/\bsrc=["\']([^"\']+)["\']/i', $image_markup, $match ) ) {
$src = html_entity_decode( $match[1], ENT_QUOTES, 'UTF-8' );
}
}
if ( ! $attachment_id && preg_match( '/(?:^|\s)wp-image-(\d+)(?:\s|$)/', $class_name, $match ) ) {
$attachment_id = absint( $match[1] );
}
$src = strtok( $src, '?#' );
if ( ! $attachment_id && $src ) {
if ( isset( $this->resolved_urls[ $src ] ) ) {
$attachment_id = $this->resolved_urls[ $src ];
} else {
$attachment_id = absint( attachment_url_to_postid( $src ) );
if ( ! $attachment_id ) {
$original_src = preg_replace( '/-\d+x\d+(?=\.(?:avif|gif|jpe?g|png|webp)$)/i', '', $src );
$attachment_id = $original_src !== $src ? absint( attachment_url_to_postid( $original_src ) ) : 0;
}
$this->resolved_urls[ $src ] = $attachment_id;
}
}
return absint( apply_filters( 'aiparently_image_attachment_id', $attachment_id, $src, $image_markup ) );
}
}