` krijgen zonder iets te slopen.
*
* Drie routes leiden naar dezelfde afbeelding — het attributenfilter, de
* contentfilter en `render_block` — en ze kunnen elkaar overlappen. Alles wat
* met bestaande markup te maken heeft staat daarom hier, op één plek, zodat de
* drie filters alleen nog hoeven te beslissen wélke afbeelding aan de beurt is.
*
* ── Twee regels die deze klasse afdwingt ────────────────────────────────────
*
* 1. Nooit twee keer. `render_block` draait binnen `the_content` (prioriteit 9)
* en `wp_filter_content_tags` daarna (prioriteit 12), dus dezelfde `
`
* passeert ons vaak twee keer. De tweede keer is een no-op.
*
* 2. Een handmatige `object-position` wint altijd. WordPress schrijft er zelf
* één in bij `core/cover`, `core/media-text` en sinds 7.0 ook `core/image`
* met een focalPoint. Dat is een expliciete keuze van de redacteur op dat
* ene blok; die overschrijven we niet met een instelling van de afbeelding.
*
* @package Yinga\Focus
*/
declare( strict_types = 1 );
namespace Yinga\Focus\Frontend;
use Yinga\Focus\Support\Naming;
use WP_HTML_Tag_Processor;
defined( 'ABSPATH' ) || exit;
final class Markup {
/**
* Niet instantieerbaar.
*/
private function __construct() {}
/**
* Voegt class en style toe aan een attributen-array.
*
* `class` en `style` worden samengevoegd met wat er al stond; al het andere
* wordt gezet. Bestaande waarden overschrijven we nooit stilzwijgend.
*
* @param array $attr Bestaande attributen.
* @param array $add Wat erbij moet.
* @return array
*/
public static function merge_attributes( array $attr, array $add ): array {
if ( array() === $add ) {
return $attr;
}
$class = isset( $attr['class'] ) ? trim( (string) $attr['class'] ) : '';
$style = isset( $attr['style'] ) ? trim( (string) $attr['style'] ) : '';
$attr['class'] = '' === $class ? $add['class'] : $class . ' ' . $add['class'];
$attr['style'] = self::merge_style( $style, $add['style'] );
foreach ( $add as $key => $value ) {
if ( 'class' !== $key && 'style' !== $key ) {
$attr[ $key ] = $value;
}
}
return $attr;
}
/**
* Plakt twee style-declaraties aan elkaar.
*
* @param string $existing Bestaande style, mag leeg zijn.
* @param string $add Wat erbij moet, eindigt al op een puntkomma.
* @return string
*/
public static function merge_style( string $existing, string $add ): string {
$existing = trim( $existing );
if ( '' === $existing ) {
return $add;
}
return rtrim( $existing, "; \t\n\r" ) . ';' . $add;
}
/**
* Heeft deze `
` al een positie — van ons of van iemand anders?
*
* @param string $class Het class-attribuut.
* @param string $style Het style-attribuut.
* @return array{done: bool, positioned: bool} `done` = wij waren hier al,
* `positioned` = er staat al een
* object-position.
*/
public static function inspect( string $class, string $style ): array {
$classes = preg_split( '/\s+/', trim( $class ) ) ?: array();
return array(
'done' => in_array( Naming::css_class( 'image' ), $classes, true ),
'positioned' => false !== stripos( $style, 'object-position' ),
);
}
/**
* Leest het beeldformaat uit de classes van een content-afbeelding.
*
* WordPress zet er `size-large`, `size-thumbnail` en dergelijke op. Dat is
* de enige manier om bij een `
` uit de post-content te weten welk
* formaat er geserveerd wordt — en dus of het hard gecropt is.
*
* @param string $class Het class-attribuut.
* @return string|null Het formaat, of null als het er niet in staat.
*/
public static function size_from_class( string $class ): ?string {
if ( ! preg_match( '/(?:^|\s)size-([A-Za-z0-9_-]+)/', $class, $match ) ) {
return null;
}
return $match[1];
}
/**
* Past de focus toe op de eerste `
` in een stuk HTML.
*
* Gebruikt WP_HTML_Tag_Processor en geen reguliere expressie: die parser
* kent de echte HTML-regels, laat de rest van de markup met rust en kan
* niet in een attribuutwaarde struikelen.
*
* @param string $html De markup.
* @param int $id Attachment-ID.
* @param ApplyPolicy $policy De policy.
* @param string|null $size Het beeldformaat, of null om het uit de classes te lezen.
* @return string De markup, ongewijzigd als er niets toe te passen viel.
*/
public static function apply( string $html, int $id, ApplyPolicy $policy, ?string $size = null ): string {
if ( '' === $html || $id <= 0 || ! class_exists( WP_HTML_Tag_Processor::class ) ) {
return $html;
}
// Goedkope voorcontrole vóór het parsen: staat er überhaupt een img in?
if ( false === stripos( $html, '
next_tag( array( 'tag_name' => 'IMG' ) ) ) {
return $html;
}
$class = (string) ( $tags->get_attribute( 'class' ) ?? '' );
$style = (string) ( $tags->get_attribute( 'style' ) ?? '' );
$state = self::inspect( $class, $style );
if ( $state['done'] ) {
return $html;
}
if ( null === $size ) {
$size = self::size_from_class( $class );
}
if ( ! $policy->should_apply( $id, $size ) ) {
return $html;
}
$add = $policy->attributes( $id, ! $state['positioned'] );
if ( array() === $add ) {
return $html;
}
$tags->add_class( $add['class'] );
$tags->set_attribute( 'style', self::merge_style( $style, $add['style'] ) );
foreach ( $add as $key => $value ) {
if ( 'class' !== $key && 'style' !== $key ) {
$tags->set_attribute( $key, $value );
}
}
return $tags->get_updated_html();
}
}