|null $profile * @return array */ public static function notes( array $draft, ?array $profile, string $host, int $now ): array { $notes = array(); $html = $draft['content']; if ( '' === trim( $draft['title'] ) ) { $notes[] = self::note( 'no_title', 'warning', __( 'The title is empty', 'forensix-digital' ), __( 'Visitors and search engines see the title first. Give the page a clear title.', 'forensix-digital' ) ); } elseif ( mb_strlen( trim( $draft['title'] ) ) > 70 ) { $notes[] = self::note( 'long_title', 'info', __( 'The title is long', 'forensix-digital' ), __( 'Search results usually cut a title after about 60 characters. Put the important words first.', 'forensix-digital' ) ); } $placeholders = array_unique( array_merge( ContentRules::placeholders( $draft['title'] ), ContentRules::placeholders( $html ) ) ); if ( array() !== $placeholders ) { $notes[] = self::note( 'placeholder', 'warning', __( 'Placeholder text is still in the page', 'forensix-digital' ), __( 'Lorem ipsum text or an address like info@example.com makes a page look unfinished. Replace it with the real text.', 'forensix-digital' ) ); } $empty = ContentRules::empty_elements( $html ); if ( $empty > 0 ) { $notes[] = self::note( 'empty_elements', 'info', /* translators: %d: number of empty elements. */ sprintf( _n( '%d empty paragraph or heading', '%d empty paragraphs or headings', $empty, 'forensix-digital' ), $empty ), __( 'Empty elements add blank space, and screen readers announce empty headings. Remove them.', 'forensix-digital' ) ); } $images = self::images_without_alt( $html ); if ( $images > 0 ) { $notes[] = self::note( 'image_alt', 'info', /* translators: %d: number of images. */ sprintf( _n( '%d image has no description', '%d images have no description', $images, 'forensix-digital' ), $images ), __( 'People who cannot see an image rely on its alternative text. Describe what the image shows, or leave it empty on purpose only for pure decoration.', 'forensix-digital' ) ); } if ( self::skips_heading_level( $html ) ) { $notes[] = self::note( 'heading_order', 'info', __( 'Headings skip a level', 'forensix-digital' ), __( 'A heading level 2 followed by level 4, for example, confuses screen readers and the outline of the page. Use the next level down.', 'forensix-digital' ) ); } if ( 1 === preg_match( '/]*\bhref\s*=\s*["\'](?:#|https?:\/\/|)["\']/i', $html ) ) { $notes[] = self::note( 'empty_link', 'warning', __( 'A link has no real address', 'forensix-digital' ), __( 'A link that points to "#" or to nothing leads nowhere. Add the address or remove the link.', 'forensix-digital' ) ); } $dates = ContentRules::past_event_dates( $html, $now ); if ( array() !== $dates ) { $notes[] = self::note( 'past_date', 'info', /* translators: %s: list of dates. */ sprintf( __( 'The text mentions a date that has passed: %s', 'forensix-digital' ), implode( ', ', $dates ) ), __( 'Check that the date is still right, or update the text.', 'forensix-digital' ) ); } if ( null !== $profile ) { $wrong = ContactConsistency::mismatches( $html, $profile, $host ); if ( array() !== array_merge( $wrong['phones'], $wrong['emails'] ) ) { $notes[] = self::note( 'contact', 'warning', __( 'A phone number or email address differs from your business details', 'forensix-digital' ), __( 'Check the contact details in this page against Business details.', 'forensix-digital' ) ); } } if ( 'post' === $draft['type'] && ! $draft['featured'] ) { $notes[] = self::note( 'no_featured', 'info', __( 'There is no featured image', 'forensix-digital' ), __( 'Many themes and social networks show the featured image next to the post.', 'forensix-digital' ) ); } return $notes; } /** * Builds one note. * * @param string $id Stable id. * @param string $level "warning" or "info". * @param string $title Short sentence. * @param string $hint What to do. * @return array{id: string, level: string, title: string, hint: string} */ private static function note( string $id, string $level, string $title, string $hint ): array { return array( 'id' => $id, 'level' => $level, 'title' => $title, 'hint' => $hint, ); } /** * Number of images without alternative text. An empty alt attribute counts too, because the * author should decide that on purpose. * * @param string $html Content. */ public static function images_without_alt( string $html ): int { $count = 0; if ( false !== preg_match_all( '/]*>/i', $html, $tags ) ) { foreach ( $tags[0] as $tag ) { if ( 1 !== preg_match( '/\balt\s*=\s*"[^"]*\S[^"]*"|\balt\s*=\s*\'[^\']*\S[^\']*\'/i', $tag ) ) { ++$count; } } } return $count; } /** * Whether a heading is more than one level deeper than the one before it. * * @param string $html Content. */ public static function skips_heading_level( string $html ): bool { if ( false === preg_match_all( '/ 0 && $level > $previous + 1 ) { return true; } $previous = $level; } return false; } }