private function class_name_updates_to_attributes_updates() {
if ( count( $this->classname_updates ) === 0 ) {
return;
}
$existing_class = $this->get_enqueued_attribute_value( 'class' );
if ( null === $existing_class || true === $existing_class ) {
$existing_class = '';
}
if ( false === $existing_class && isset( $this->attributes['class'] ) ) {
$existing_class = substr(
$this->html,
$this->attributes['class']->value_starts_at,
$this->attributes['class']->value_length
);
}
if ( false === $existing_class ) {
$existing_class = '';
}
/**
* Updated "class" attribute value.
*
* This is incrementally built while scanning through the existing class
* attribute, skipping removed classes on the way, and then appending
* added classes at the end. Only when finished processing will the
* value contain the final new value.
* @var string $class
*/
$class = '';
/**
* Tracks the cursor position in the existing
* class attribute value while parsing.
*
* @var int $at
*/
$at = 0;
/**
* Indicates if there's any need to modify the existing class attribute.
*
* If a call to `add_class()` and `remove_class()` wouldn't impact
* the `class` attribute value then there's no need to rebuild it.
* For example, when adding a class that's already present or
* removing one that isn't.
*
* This flag enables a performance optimization when none of the enqueued
* class updates would impact the `class` attribute; namely, that the
* processor can continue without modifying the input document, as if
* none of the `add_class()` or `remove_class()` calls had been made.
*
* This flag is set upon the first change that requires a string update.
*
* @var bool $modified
*/
$modified = false;
// Remove unwanted classes by only copying the new ones.
$existing_class_length = strlen( $existing_class );
while ( $at < $existing_class_length ) {
// Skip to the first non-whitespace character.
$ws_at = $at;
$ws_length = strspn( $existing_class, " \t\f\r\n", $ws_at );
$at += $ws_length;
// Capture the class name – it's everything until the next whitespace.
$name_length = strcspn( $existing_class, " \t\f\r\n", $at );
if ( 0 === $name_length ) {
// If no more class names are found then that's the end.
break;
}
$name = substr( $existing_class, $at, $name_length );
$at += $name_length;
// If this class is marked for removal, start processing the next one.
$remove_class = (
isset( $this->classname_updates[ $name ] ) &&
self::REMOVE_CLASS === $this->classname_updates[ $name ]
);
// If a class has already been seen then skip it; it should not be added twice.
if ( ! $remove_class ) {
$this->classname_updates[ $name ] = self::SKIP_CLASS;
}
if ( $remove_class ) {
$modified = true;
continue;
}
/*
* Otherwise, append it to the new "class" attribute value.
*
* There are options for handling whitespace between tags.
* Preserving the existing whitespace produces fewer changes
* to the HTML content and should clarify the before/after
* content when debugging the modified output.
*
* This approach contrasts normalizing the inter-class
* whitespace to a single space, which might appear cleaner
* in the output HTML but produce a noisier change.
*/
$class .= substr( $existing_class, $ws_at, $ws_length );
$class .= $name;
}
// Add new classes by appending those which haven't already been seen.
foreach ( $this->classname_updates as $name => $operation ) {
if ( self::ADD_CLASS === $operation ) {
$modified = true;
$class .= strlen( $class ) > 0 ? ' ' : '';
$class .= $name;
}
}
$this->classname_updates = array();
if ( ! $modified ) {
return;
}
if ( strlen( $class ) > 0 ) {
$this->set_attribute( 'class', $class );
} else {
$this->remove_attribute( 'class' );
}
}