post_type && 0 === strpos( (string) $post->post_mime_type, 'image/' ) ) { $text = self::from_source( $post, $source ); if ( '' === $text && DevLab_ALT_Settings::SOURCE_FILENAME !== $source ) { $text = self::from_source( $post, DevLab_ALT_Settings::SOURCE_FILENAME ); } /** * Filters the alt text that will be filled in for an attachment. * Return an empty string to leave the image without alt text. * * @param string $text Alt text found for the attachment. * @param int $attachment_id Attachment post ID. * @param string $source The source chosen in the settings. */ $filtered = apply_filters( 'devlab_alt_text', $text, $attachment_id, $source ); $text = is_string( $filtered ) ? self::tidy( $filtered ) : ''; } self::$cache[ $key ] = $text; return $text; } /** * Reads one source from the attachment post. * * @param WP_Post $post Attachment post. * @param string $source One of the DevLab_ALT_Settings::SOURCE_* values. * @return string Tidied text, possibly empty. */ private static function from_source( $post, $source ) { switch ( $source ) { case DevLab_ALT_Settings::SOURCE_CAPTION: $text = $post->post_excerpt; break; case DevLab_ALT_Settings::SOURCE_DESCRIPTION: $text = $post->post_content; break; case DevLab_ALT_Settings::SOURCE_FILENAME: $text = self::clean_filename( $post->ID ); break; case DevLab_ALT_Settings::SOURCE_TITLE: default: $text = self::humanize_if_file_name( $post->post_title ); break; } $text = self::tidy( $text ); return self::is_meaningless( $text ) ? '' : $text; } /** * WordPress fills a new upload's title with its file name ("my-holiday_photo"), * so a title with no spaces but with a hyphen or an underscore is cleaned * up like a file name. Any other title is returned untouched. * * @param mixed $title Attachment title. * @return mixed */ private static function humanize_if_file_name( $title ) { if ( ! is_string( $title ) || 1 === preg_match( '/\s/', $title ) || 1 !== preg_match( '/[-_]/', $title ) ) { return $title; } return self::clean_name( $title ); } /** * Whether a text would be useless as alt text: no word of three letters * or more ("12-34"), a camera, phone or screenshot name followed only by * numbers, dates and times ("IMG 4032", "IMG-20261024-WA0012", "PXL 2026 * 123 MP", "Screenshot 2026-10-24 at 3.20.11 P.M.", "Captura de Tela * 2026-10-24 às 15.20.11"), or a long hexadecimal hash used as a file name. * * @param string $text Tidied text. * @return bool */ private static function is_meaningless( $text ) { // "&" must not count as the word "amp". $text = html_entity_decode( $text, ENT_QUOTES, 'UTF-8' ); if ( '' === $text || 1 !== preg_match( '/\p{L}{3,}/u', $text ) ) { return true; } // A known prefix, then only separators, digits and date/time words. Each // repetition consumes at least one character, so this is linear. $camera = '/^(?:img|dsc[nf]?|pxl|mvimg|vid|screenshot|screen shot|whatsapp (?:image|video)|image|imagem|imagen|photo|picture|pic|foto|bild|bildschirmfoto|schermafbeelding|captura de tela|captura de ecrã|captura de pantalla|capture d.écran)' . '(?:[\s._:,\-()#]|\d|(? self::MAX_INPUT ) { $text = function_exists( 'mb_strcut' ) ? mb_strcut( $text, 0, self::MAX_INPUT, 'UTF-8' ) : substr( $text, 0, self::MAX_INPUT ); } // Removes tags and line breaks, collapses spaces, cuts at MAX_LENGTH. return trim( wp_html_excerpt( $text, self::MAX_LENGTH, '…' ) ); } /** * Turns the attachment's file name into readable text: see clean_name(). * * @param int $attachment_id Attachment post ID. * @return string */ private static function clean_filename( $attachment_id ) { $file = get_post_meta( $attachment_id, '_wp_attached_file', true ); if ( ! is_string( $file ) || '' === $file ) { return ''; } return self::clean_name( pathinfo( wp_basename( $file ), PATHINFO_FILENAME ) ); } /** * Turns a file name (without extension) into readable text: no "-scaled" * or "-e1234567890" (edited image) suffix, no "-1" to "-9" duplicate-upload * suffix, hyphens and underscores become spaces and, when the name has no * capital letter at all, the first letter becomes one ("my-holiday_photo-1" * becomes "My holiday photo"; "iPhone-case" keeps its "iPhone"). * * @param string $name File name without its extension. * @return string */ private static function clean_name( $name ) { $name = preg_replace( '/-[1-9]$/', '', (string) $name ); $name = preg_replace( '/-(?:scaled|e\d{10,})$/i', '', (string) $name ); $name = preg_replace( '/[-_\s]+/', ' ', (string) $name ); $name = trim( (string) $name ); if ( '' === $name ) { return ''; } if ( 1 === preg_match( '/\p{Lu}/u', $name ) ) { return $name; } if ( function_exists( 'mb_substr' ) && function_exists( 'mb_strtoupper' ) ) { return mb_strtoupper( mb_substr( $name, 0, 1, 'UTF-8' ), 'UTF-8' ) . mb_substr( $name, 1, null, 'UTF-8' ); } return ucfirst( $name ); } }