Skip to content
Closed
164 changes: 164 additions & 0 deletions lib/experimental/html/class-wp-html-tag-processor.php
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
* @since 6.2.0
*/
class WP_HTML_Tag_Processor {
const MAX_BOOKMARKS = 10;

/**
* The HTML document to parse.
Expand Down Expand Up @@ -362,6 +363,15 @@ class WP_HTML_Tag_Processor {
*/
private $classname_updates = array();

/**
* Tracks a semantic location in the original HTML that shuffles
* with the updates applied to the document.
*
* @since 6.2.0
* @var array
*/
private $bookmarks = array();

const ADD_CLASS = true;
const REMOVE_CLASS = false;
const SKIP_CLASS = null;
Expand Down Expand Up @@ -479,6 +489,47 @@ public function next_tag( $query = null ) {
return true;
}


/**
* Sets a bookmark in the HTML document.
*
* @param string $name Identifies this particular bookmark.
* @return false|void
*/
public function set_bookmark( $name ) {
if ( null === $this->tag_name_starts_at ) {
return false;
}

if ( ! array_key_exists( $name, $this->bookmarks ) && count( $this->bookmarks ) > self::MAX_BOOKMARKS ) {
return false;
}

$this->bookmarks[ $name ] = new WP_HTML_Text_Replacement(
$this->tag_name_starts_at - 1,
$this->tag_ends_at,
''
);
}


/**
* Removes a bookmark once it's not necessary anymore.
*
* @param string $name Name of the bookmark to remove.
* @return bool
*/
public function release_bookmark( $name ) {

if ( ! array_key_exists( $name, $this->bookmarks ) ) {
return false;
}

unset( $this->bookmarks[ $name ] );
return true;
}


/**
* Skips the contents of the title and textarea tags until an appropriate
* tag closer is found.
Expand Down Expand Up @@ -1102,11 +1153,115 @@ private function apply_attributes_updates() {
$this->updated_html .= substr( $this->html, $this->updated_bytes, $diff->start - $this->updated_bytes );
$this->updated_html .= $diff->text;
$this->updated_bytes = $diff->end;

foreach ( $this->bookmarks as $name => &$position ) {
$update_head = $position->start >= $diff->start;
$update_tail = $position->end >= $diff->start;

if ( ! $update_head && ! $update_tail ) {
continue;
}

/*
* If a change is made that encompasses an entire bookmark then we
* have to remove the bookmark as the semantic place it pointed to
* no longer exists. It could seem like we can let it remain as
* long as we haven't removed the text, but if the text is different
* then the place likely doesn't exist at all either and we need to
* start over.
*/
if ( $diff->start <= $position->start && $diff->end >= $position->end ) {
unset( $this->bookmarks[ $name ] );
continue;
}

$delta = strlen( $diff->text ) - ( $diff->end - $diff->start );

if ( $update_head ) {
$position->start += $delta;
}

if ( $update_tail ) {
$position->end += $delta;
}
}
}

$this->attribute_updates = array();
}

/**
* Move the current pointer in the Tag Processor to a given bookmark's location.
*
* @param string $bookmark_name Name of bookmark to which to rewind.
* @return bool
* @throws Exception Throws on invalid bookmark name if WP_DEBUG set.
*/
public function seek( $bookmark_name ) {
if ( ! array_key_exists( $bookmark_name, $this->bookmarks ) ) {
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
throw new Exception( 'Invalid bookmark name' );
}
return false;
}

// Apply all the updates.
$this->apply_string_diffs();

$start = $this->bookmarks[ $bookmark_name ]->start;
$this->parsed_bytes = $start;
$this->updated_bytes = $start;
$this->updated_html = substr( $this->html, 0, $this->parsed_bytes );
return $this->next_tag();
}

public function dangerously_get_contents( $start_bookmark, $end_bookmark, $region = 'outer' ) {
if (
empty( $start_bookmark ) ||
empty( $end_bookmark ) ||
! isset( $this->bookmarks[ $start_bookmark ], $this->bookmarks[ $end_bookmark ] ) ||
( $start_bookmark === $end_bookmark && $region !== 'outer' )
) {
return false;
}

$start = $this->bookmarks[ $start_bookmark ];
$end = $this->bookmarks[ $end_bookmark ];

if ( $start->start > $end->start || $start->end > $end->end ) {
return false;
}

$start = 'outer' === $region ? $start->start : $start->end + 1;
$end = 'outer' === $region ? $end->end + 1 : $end->start - 1;

return substr( $this->html, $start, $end - $start );
}

public function dangerously_replace( $start_bookmark, $end_bookmark, $text, $region = 'outer' ) {
if (
empty( $start_bookmark ) ||
empty( $end_bookmark ) ||
! isset( $this->bookmarks[ $start_bookmark ], $this->bookmarks[ $end_bookmark ] ) ||
( $start_bookmark === $end_bookmark && $region !== 'outer' )
) {
return false;
}

$start = $this->bookmarks[ $start_bookmark ];
$end = $this->bookmarks[ $end_bookmark ];

if ( $start->start > $end->start || $start->end > $end->end ) {
return false;
}

$start = 'outer' === $region ? $start->start : $start->end + 1;
$end = 'outer' === $region ? $end->end + 1 : $end->start - 1;

$this->attribute_updates[] = new WP_HTML_Text_Replacement( $start, $end, $text );
$this->apply_attributes_updates();
}

/**
* Sort function to arrange objects with a start property in ascending order.
*
Expand Down Expand Up @@ -1416,6 +1571,15 @@ public function get_updated_html() {
return $this->updated_html . substr( $this->html, $this->updated_bytes );
}

return $this->apply_string_diffs();
}

/**
* I just ripped out the part I need to call in the rewind().
*
* @TODO separate it more cleanly.
*/
private function apply_string_diffs() {
/*
* Parsing is in progress – let's apply the attribute updates without moving on to the next tag.
*
Expand Down
Loading