Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7a093de
HTML API: Add stack helpers for adoption and format reconstruction.
sirreal Jul 2, 2026
4880c1e
HTML API: Implement adoption agency and format reconstruction.
sirreal Jul 2, 2026
764adbd
HTML API: Handle FORM end tags without bailing.
sirreal Jul 2, 2026
4417c38
Merge branch 'html-apl/implement-aaa-afr' into html-api/foster-parenting
sirreal Jul 8, 2026
8b79ac3
Tests: Re-triage html5lib skips against Web Platform Tests data.
sirreal Jul 8, 2026
8d80c61
HTML API: Support foster parenting in the HTML Processor.
sirreal Jul 8, 2026
77c2c9d
HTML API: Add tests covering foster parenting behavior.
sirreal Jul 8, 2026
25d1985
HTML API: Only clear table contexts up to HTML table-part elements.
sirreal Jul 8, 2026
9d4a362
HTML API: Require opting in to foster parenting support.
sirreal Jul 8, 2026
e9526ae
HTML API: Specify foster parenting presentation modes.
sirreal Jul 8, 2026
6db1b4b
HTML API: Rename the foster parenting opt-in for its ordering trade.
sirreal Jul 8, 2026
e2d5ec1
HTML API: Present foster-parented content in document order by default.
sirreal Jul 8, 2026
5a115bf
HTML API: Contain deferral aborts within the error interface.
sirreal Jul 8, 2026
2459eed
Comments: allow the Notes @mention chip markup in comment content.
adamsilverstein Jul 23, 2026
b0e62d8
Docs: Require view config filter callbacks to return the container.
jorgefilipecosta Jul 23, 2026
5fd5a8e
View config: reject shape-mismatched merges, define empty-array seman…
jorgefilipecosta Jul 23, 2026
777b966
Merge remote-tracking branch 'upstream/trunk' into html-api/foster-pa…
sirreal Jul 23, 2026
522ebaa
Merge remote-tracking branch 'upstream/trunk' into html-api/foster-pa…
sirreal Jul 23, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 66 additions & 7 deletions src/wp-includes/class-wp-view-config-data.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@
* key by key (an associative array merges member by member, a nested `null`
* deletes just that leaf, a scalar replaces just that value), while `set()`
* swaps the whole value. A nested `null` deletes just the leaf it names in
* every case. Each patch also declares the configuration schema
* every case. A patch value whose shape does not match the current value —
* an associative array where a list lives, or the reverse — is rejected with
* a notice rather than merged, and an empty array under `merge()` is a
* no-op. Each patch also declares the configuration schema
* version it was written against (currently 1), so a future WordPress release
* that changes the configuration shape can migrate existing patches forward
* instead of breaking them.
Expand Down Expand Up @@ -159,10 +162,12 @@ public function apply_filters( $kind, $name ) {
* individual list members.
*
* A change that declares an unsupported schema version is rejected and does
* not alter anything. Callbacks mutate the container in place, so there is no
* need to return it; any returned value is ignored. Callbacks must not replace
* the container with a different value, as later callbacks receive whatever the
* the previous one returned.
* not alter anything. As with any filter, each callback's return value is
* passed to the next callback as `$data`, so callbacks must return the
* container they received: a callback that returns nothing, or any other
* value, hands that result to every callback hooked at a later priority
* instead of the container. Since the write methods return the container,
* a callback can end with `return $data->merge( $patch, $version );`.
*
* @since 7.1.0
*
Expand Down Expand Up @@ -306,6 +311,12 @@ public function remove( array $spec, int $version ) {
* stops inheriting core's future additions to it — but it's useful when a
* contributor needs to pin a list to an exact set of members.
*
* The shape rule applies here too: a patch value whose shape does not match
* the current value — an associative array where a list lives, or a
* non-empty list where an associative value lives — is rejected with a
* notice and leaves the current value unchanged. An empty array is exempt,
* so replacing a list with an empty list still clears it.
*
* A patch that declares an unsupported schema version is rejected and does
* not change anything.
*
Expand Down Expand Up @@ -352,6 +363,13 @@ public function replace( array $patch, int $version ) {
* - default_layouts will be updated so that newField is appended to the badgeFields.
* - view_list will be updated so that the view with slug 'table' has its title changed to 'New title'.
*
* A patch value only merges into a current value of the same shape: an
* associative array where a list lives, or a non-empty list where an
* associative value lives, is rejected with a notice and leaves the current
* value unchanged. An empty array merges nothing and is a no-op — clear a
* list with replace() and an empty list, or reset a key to its default with
* a top-level `null`.
*
* A patch that declares an unsupported schema version is rejected and does
* not change anything.
*
Expand Down Expand Up @@ -475,6 +493,15 @@ private function strip_nulls( $value ) {
* $replace_lists flag is carried down through associative nesting so that,
* under replace(), every list reached along the way is swapped wholesale.
*
* An array in $incoming only merges into a current value of the same shape.
* A non-empty mismatch — an associative array where a list lives, or a
* non-empty list where an associative value lives — is reported with
* _doing_it_wrong() and leaves the current value unchanged, so a malformed
* patch cannot silently destroy configuration. An empty array is
* shape-ambiguous and merges nothing, so it is a no-op: clearing a list is
* spelled replace() with an empty list, and resetting a key is spelled
* `null`.
*
* @since 7.1.0
*
* @param mixed $current The current value.
Expand All @@ -491,20 +518,48 @@ private function merge_properties( $current, $incoming, $replace_lists ) {

// Numerical indexed arrays are expected to be lists (sequential integer keys starting at 0).
if ( array_is_list( $incoming ) ) {
// A non-empty list only lands where a list (or nothing) lives, under
// merge() and replace() alike. An empty array is shape-ambiguous and
// exempt, so replace() with an empty list can still clear a list.
if ( array() !== $incoming && is_array( $current ) && ! array_is_list( $current ) && array() !== $current ) {
_doing_it_wrong(
__METHOD__,
esc_html__( 'A view configuration patch value must match the shape of the value it patches: a list merges into a list, and an associative array into an associative array.' ),
'7.1.0'
);
return $current;
}

// replace() takes an incoming list as-is; merge() merges it by member identity.
if ( $replace_lists ) {
// As-is except for nulls: a list swapped in wholesale has no
// existing leaf for a null to delete (the same rationale as
// set()), so a null member is dropped rather than stored.
return $this->strip_nulls( $incoming );
}

// An empty list has no members to merge, and an empty array is
// shape-ambiguous, so merging one is a no-op rather than a reset.
if ( array() === $incoming ) {
return $current;
}

return $this->merge_list_by_identity(
is_array( $current ) && array_is_list( $current ) ? $current : array(),
$incoming
);
}

// Consider any other array as associative (keys are strings).
if ( is_array( $current ) && array_is_list( $current ) && array() !== $current ) {
_doing_it_wrong(
__METHOD__,
esc_html__( 'A view configuration patch value must match the shape of the value it patches: a list merges into a list, and an associative array into an associative array.' ),
'7.1.0'
);
return $current;
}

$result = is_array( $current ) && ! array_is_list( $current ) ? $current : array();
foreach ( $incoming as $key => $value ) {
// A null patch value deletes the property.
Expand Down Expand Up @@ -601,7 +656,9 @@ private function remove_list_member( array $members, $identity ) {
* A member of the incoming list whose identity matches one already present
* merges into it in place, keeping its position; an unmatched member is
* appended to the end, except a literal `null`, which carries no identity
* and holds nothing to merge and so is dropped. A matched member's contents
* and holds nothing to merge and so is dropped. An appended member has no
* existing leaf for a nested `null` to delete (the same rationale as set()),
* so its nulls are stripped rather than stored. A matched member's contents
* merge recursively with the same rules (merge_properties), so the
* identity-aware merge applies at
* any nesting level: each key named by the patch is substituted while the
Expand Down Expand Up @@ -637,7 +694,9 @@ private function merge_list_by_identity( array $current, array $incoming ) {
}
}
if ( null === $index ) {
$result[] = $item;
// An appended member has no existing leaf for a nested null to
// delete, so nulls are dropped rather than stored.
$result[] = $this->strip_nulls( $item );
continue;
}

Expand Down
5 changes: 5 additions & 0 deletions src/wp-includes/default-filters.php
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,11 @@
add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
add_filter( 'comment_flood_filter', 'wp_throttle_comment_flood', 10, 3 );
add_filter( 'pre_comment_content', 'wp_rel_ugc', 15 );

// Note mention chips in comment content: allow `span` through comment kses,
// then reduce its classes to the mention tokens right after `wp_filter_kses`.
add_filter( 'wp_kses_allowed_html', '_wp_kses_allow_note_mention_span', 10, 2 );
add_filter( 'pre_comment_content', '_wp_kses_sanitize_note_mention_classes', 11 );
add_filter( 'comment_email', 'antispambot' );
add_filter( 'option_tag_base', '_wp_filter_taxonomy_base' );
add_filter( 'option_category_base', '_wp_filter_taxonomy_base' );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,11 @@ public function insert_marker(): void {
*/
public function push( WP_HTML_Token $token ) {
/*
* > If there are already three elements in the list of active formatting elements after the last marker,
* > if any, or anywhere in the list if there are no markers, that have the same tag name, namespace, and
* > attributes as element, then remove the earliest such element from the list of active formatting
* > elements. For these purposes, the attributes must be compared as they were when the elements were
* > created by the parser; two elements have the same attributes if all their parsed attributes can be
* > paired such that the two attributes in each pair have identical names, namespaces, and values
* > (the order of the attributes does not matter).
* The "Noah's Ark clause", which limits the list to three elements sharing
* a tag name, namespace, and attributes, requires reading the attributes
* of the source tags and is enforced by the HTML Processor before pushing.
*
* @todo Implement the "Noah's Ark clause" to only add up to three of any given kind of formatting elements to the stack.
* @see WP_HTML_Processor::push_onto_active_formatting_elements
*/
// > Add element to the list of active formatting elements.
$this->stack[] = $token;
Expand Down Expand Up @@ -150,6 +146,82 @@ public function remove_node( WP_HTML_Token $token ) {
return false;
}

/**
* Returns the position of a node in the list of active formatting elements.
*
* Positions are counted from the start of the list: the earliest entry
* is at position zero.
*
* @since 7.1.0
*
* @param WP_HTML_Token $token Find this node in the list of active formatting elements.
* @return int|null Position of the node, or `null` if it isn't in the list.
*/
public function position_of( WP_HTML_Token $token ): ?int {
foreach ( $this->stack as $position => $item ) {
if ( $token === $item ) {
return $position;
}
}

return null;
}

/**
* Removes the node at the given position in the list of active formatting elements.
*
* @since 7.1.0
*
* @param int $position Remove the node at this position, counting from the start of the list.
* @return bool Whether a node was removed, false when the position was out of range.
*/
public function remove_at( int $position ): bool {
if ( $position < 0 || $position >= count( $this->stack ) ) {
return false;
}

array_splice( $this->stack, $position, 1 );
return true;
}

/**
* Inserts a node at the given position in the list of active formatting elements.
*
* A node inserted at position zero becomes the earliest entry in the list,
* while one inserted at the position returned by {@see self::count} becomes
* the last (most recently added) entry.
*
* @since 7.1.0
*
* @param int $position Insert the node at this position, counting from the start of the list.
* @param WP_HTML_Token $token Insert this node.
*/
public function insert_at( int $position, WP_HTML_Token $token ): void {
array_splice( $this->stack, $position, 0, array( $token ) );
}

/**
* Replaces a node in the list of active formatting elements with another node.
*
* This is distinct from removing the existing node and pushing the new one:
* the replacement occupies the exact position of the node it replaces.
*
* @since 7.1.0
*
* @param WP_HTML_Token $old_node Node to find and replace.
* @param WP_HTML_Token $new_node Node to substitute in its place.
* @return bool Whether the node was found and replaced.
*/
public function replace_node( WP_HTML_Token $old_node, WP_HTML_Token $new_node ): bool {
$position = $this->position_of( $old_node );
if ( null === $position ) {
return false;
}

$this->stack[ $position ] = $new_node;
return true;
}

/**
* Steps through the stack of active formatting elements, starting with the
* top element (added first) and walking downwards to the one added last.
Expand Down
Loading
Loading