Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tag processor/dangerously replace contents #46080

Closed
wants to merge 11 commits into from
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