safe_rules; } public function can_fix_one( $rule ) { return in_array( $rule, array_merge( $this->safe_rules, $this->reviewed_rules ), true ); } public function fixable_issues( $issues ) { return array_intersect_key( (array) $issues, array_flip( $this->safe_rules ) ); } public function fixable_total( $issues ) { return array_sum( array_map( 'intval', $this->fixable_issues( $issues ) ) ); } public function fix_one( $content, $rule, $occurrence ) { $content = (string) $content; $occurrence = max( 0, (int) $occurrence ); if ( ! $this->can_fix_one( $rule ) ) { return array( 'content' => $content, 'changed' => false ); } if ( 'empty_paragraph' === $rule ) { $pattern = '/(?:\s*)?
]*>\s*(?: | |\s|
)*<\/p>\s*(?:)?/is';
return $this->replace_occurrence( $content, $pattern, $occurrence, function() { return ''; } );
}
if ( 'duplicate_ids' === $rule ) { return $this->rename_duplicate_id( $content, $occurrence ); }
if ( 'heading_skip' === $rule || 'multiple_h1' === $rule ) { return $this->repair_heading( $content, $rule, $occurrence ); }
if ( 'nested_strong' === $rule || 'nested_em' === $rule ) {
$tag = 'nested_strong' === $rule ? 'strong' : 'em';
$pattern = '/<' . $tag . '\\b([^>]*)>\\s*<' . $tag . '\\b[^>]*>(.*?)<\\/' . $tag . '>\\s*<\\/' . $tag . '>/is';
return $this->replace_occurrence( $content, $pattern, $occurrence, function( $match ) use ( $tag ) { return '<' . $tag . $match[1] . '>' . $match[2] . '' . $tag . '>'; } );
}
if ( 'missing_alt' === $rule ) {
return $this->replace_occurrence( $content, '/]*\\salt=["\'])[^>]*>/i', $occurrence, function( $match ) { return preg_replace( '/\\s*\\/?\\s*>$/', ' alt=""$0', $match[0], 1 ); } );
}
$current = 0; $changed = false;
$updated = preg_replace_callback( '/]*target=["\']_blank["\'][^>]*>/i', function( $match ) use ( &$current, &$changed, $occurrence ) {
$tag = $match[0];
if ( preg_match( '/\\brel=["\'][^"\']*\\b(?:noopener|noreferrer)\\b[^"\']*["\']/i', $tag ) ) { return $tag; }
if ( $current++ !== $occurrence ) { return $tag; }
$changed = true;
if ( preg_match( '/\\brel=(["\'])(.*?)\\1/i', $tag, $rel ) ) {
$tokens = array_filter( preg_split( '/\\s+/', strtolower( trim( $rel[2] ) ) ) );
foreach ( array( 'noopener', 'noreferrer' ) as $needed ) { if ( ! in_array( $needed, $tokens, true ) ) { $tokens[] = $needed; } }
return preg_replace( '/\\brel=(["\'])(.*?)\\1/i', 'rel="' . implode( ' ', $tokens ) . '"', $tag, 1 );
}
return preg_replace( '/>$/', ' rel="noopener noreferrer">', $tag, 1 );
}, $content );
return array( 'content' => $updated, 'changed' => $changed );
}
private function rename_duplicate_id( $content, $wanted ) {
preg_match_all( '/\sid=(["\'])([^"\']+)\1/i', $content, $matches, PREG_OFFSET_CAPTURE );
$counts = array_count_values( array_map( function( $item ) { return $item[0]; }, $matches[2] ) );
$seen = array(); $candidate = 0; $changed = false;
$updated = preg_replace_callback( '/\sid=(["\'])([^"\']+)\1/i', function( $match ) use ( &$seen, &$candidate, &$changed, $wanted, $counts, $content ) {
$id = $match[2];
$seen[ $id ] = isset( $seen[ $id ] ) ? $seen[ $id ] + 1 : 1;
if ( empty( $counts[ $id ] ) || $counts[ $id ] < 2 || 1 === $seen[ $id ] ) { return $match[0]; }
if ( $candidate++ !== $wanted ) { return $match[0]; }
if ( preg_match( '/(?:url\(\s*#|xlink:href=["\']#)' . preg_quote( $id, '/' ) . '/i', $content ) ) { return $match[0]; }
$suffix = $seen[ $id ];
do { $new_id = $id . '-' . $suffix++; } while ( preg_match( '/\sid=["\']' . preg_quote( $new_id, '/' ) . '["\']/i', $content ) );
$changed = true;
return ' id=' . $match[1] . $new_id . $match[1];
}, $content );
return array( 'content' => $updated, 'changed' => $changed );
}
private function repair_heading( $content, $rule, $wanted ) {
$previous = null; $candidate = 0; $h1_seen = 0; $changed = false;
$updated = preg_replace_callback( '/
]*>/i', function ( $match ) use ( &$changes ) {
if ( preg_match( '/\salt\s*=/i', $match[0] ) ) { return $match[0]; }
$changes['missing_alt'] = isset( $changes['missing_alt'] ) ? $changes['missing_alt'] + 1 : 1;
return preg_replace( '/\s*\/?\s*>$/', ' alt=""$0', $match[0], 1 );
}, $content );
$content = preg_replace_callback( '/]*target=["\']_blank["\'][^>]*>/i', function ( $match ) use ( &$changes ) {
$tag = $match[0];
if ( preg_match( '/\brel=(["\'])(.*?)\1/i', $tag, $rel ) ) {
$tokens = preg_split( '/\s+/', trim( $rel[2] ) );
$tokens = array_filter( array_map( 'strtolower', $tokens ) );
$updated = false;
foreach ( array( 'noopener', 'noreferrer' ) as $needed ) {
if ( ! in_array( $needed, $tokens, true ) ) { $tokens[] = $needed; $updated = true; }
}
if ( ! $updated ) { return $tag; }
$tag = preg_replace( '/\brel=(["\'])(.*?)\1/i', 'rel="' . implode( ' ', $tokens ) . '"', $tag, 1 );
} else {
$tag = preg_replace( '/>$/', ' rel="noopener noreferrer">', $tag, 1 );
}
$changes['unsafe_blank_link'] = isset( $changes['unsafe_blank_link'] ) ? $changes['unsafe_blank_link'] + 1 : 1;
return $tag;
}, $content );
return array( 'content' => $content, 'changes' => $changes );
}
private function collapse_nested_tag( $content, $tag, $rule, &$changes ) {
$pattern = '/<' . $tag . '\b([^>]*)>\s*<' . $tag . '\b[^>]*>(.*?)<\/' . $tag . '>\s*<\/' . $tag . '>/is';
do {
$content = preg_replace_callback( $pattern, function ( $match ) use ( $tag, $rule, &$changes ) {
$changes[ $rule ] = isset( $changes[ $rule ] ) ? $changes[ $rule ] + 1 : 1;
return '<' . $tag . $match[1] . '>' . $match[2] . '' . $tag . '>';
}, $content, -1, $count );
} while ( $count > 0 );
return $content;
}
}