Automattic\WooCommerce\Internal\EmailEditor\WCTransactionalEmails
WCEmailTemplateChangeSummary::diff_records │ private static │ WC 1.0 Diff two flattened record sequences.
Each added_blocks / removed_blocks / copy_changes / structural_changes entry carries a path field — the index path through the parsed block tree on the side where the relevant block exists:
added_blocks[].path — core-side path (where it would land if applied).
removed_blocks[].path — post-side path (where it currently sits).
copy_changes[].path — post-side path (the merchant's renderable surface).
structural_changes[].path — post-side for matched-pair moves; the unmatched side for wrapper additions/removals; omitted for kind: 'reorder' entries (no single block).
RSM-143's selective-merge UI uses path to map per-block "Keep yours / Use core" choices back to specific blocks during merge.
Method of the class: WCEmailTemplateChangeSummary{}
No Hooks.
Returns
Array{added_blocks:Array. array<string, mixed>>, removed_blocks:array<int, array<string, mixed>>, copy_changes:array<int, array<string, mixed>>, structural_changes:array<int, array<string, mixed>>}
Usage
$result = WCEmailTemplateChangeSummary::diff_records( $core_records, $post_records ): array;
$core_records(array) (required)
.
$post_records(array) (required)
.
WCEmailTemplateChangeSummary::diff_records() WCEmailTemplateChangeSummary::diff records code
WC 10.9.1
private static function diff_records( array $core_records, array $post_records ): array {
$core_names = array_map( static fn( array $r ): string => $r['name'], $core_records );
$matches = self::lcs_matches( $core_records, $post_records );
$matched_core = array();
$matched_post = array();
foreach ( $matches as $pair ) {
$matched_core[ $pair[0] ] = true;
$matched_post[ $pair[1] ] = true;
}
$added_blocks = array();
$removed_blocks = array();
$copy_changes = array();
$structural_changes = array();
// `added_blocks` / `removed_blocks` follow the "yours → core" convention:
// `added_blocks` = blocks the merchant would gain by applying the update
// (in core, not in post). `removed_blocks` = blocks the merchant would
// lose by applying the update (in post, not in core). Same direction as
// `before` (yours) / `after` (core) on copy_changes.
//
// Pass order: matched pairs first, then unmatched. The matched-pair
// pass collects parent-name pairs whose mismatch will already produce
// a "Moved %1$s into %2$s" entry, so the unmatched-pass can suppress
// the redundant "Added/Removed %s wrapper" entry that would otherwise
// describe the same physical edit twice.
// Pass 1: classify matched pairs.
$core_name_counts = array_count_values( $core_names );
$occurrence_index = array();
$matched_core_parents = array();
$matched_post_parents = array();
foreach ( $matches as $pair ) {
$core = $core_records[ $pair[0] ];
$post_r = $post_records[ $pair[1] ];
$name = $core['name'];
$label = self::block_label( $name );
$occurrence_index[ $name ] = ( $occurrence_index[ $name ] ?? 0 ) + 1;
$total = (int) ( $core_name_counts[ $name ] ?? 1 );
if ( $core['parent_name'] !== $post_r['parent_name'] ) {
// Destination is core's parent (where the block would land after
// applying the update), not post's (where it currently sits).
$structural_changes[] = array(
'kind' => 'nest',
'description' => sprintf(
/* translators: 1: block name; 2: parent block name */
__( 'Moved %1$s into %2$s', 'woocommerce' ),
$label,
null === $core['parent_name'] ? __( 'top level', 'woocommerce' ) : self::block_label( $core['parent_name'] )
),
'path' => $post_r['path'],
);
if ( null !== $post_r['parent_name'] ) {
$matched_post_parents[ $post_r['parent_name'] ] = true;
}
if ( null !== $core['parent_name'] ) {
$matched_core_parents[ $core['parent_name'] ] = true;
}
}
if ( $core['inner_text'] !== $post_r['inner_text'] ) {
// `before` = merchant's current text (what they have now), `after` = canonical
// core text (what they would get if they applied the update). Matches the
// design's "yours" → "core" diff convention.
$copy_changes[] = array(
'block' => $label,
'before' => self::truncate_text( $post_r['inner_text'] ),
'after' => self::truncate_text( $core['inner_text'] ),
'occurrence' => $occurrence_index[ $name ],
'total' => $total,
'path' => $post_r['path'],
);
}
}//end foreach
// Pass 2: classify unmatched core. Skip wrapper entry if a matched
// pair already names this wrapper as its core-side parent — that
// matched pair's "Moved into" entry covers the same physical edit.
foreach ( $core_records as $i => $rec ) {
if ( isset( $matched_core[ $i ] ) ) {
continue;
}
if ( isset( self::STRUCTURAL_BLOCK_NAMES[ $rec['name'] ] ) ) {
if ( isset( $matched_core_parents[ $rec['name'] ] ) ) {
continue;
}
$structural_changes[] = array(
'kind' => 'nest',
'description' => sprintf(
/* translators: %s: block name */
__( 'Added %s wrapper', 'woocommerce' ),
self::block_label( $rec['name'] )
),
'path' => $rec['path'],
);
continue;
}
$added_blocks[] = array(
'name' => $rec['name'],
'label' => self::block_label( $rec['name'] ),
'path' => $rec['path'],
);
}//end foreach
// Pass 3: classify unmatched post, with the same wrapper suppression.
foreach ( $post_records as $i => $rec ) {
if ( isset( $matched_post[ $i ] ) ) {
continue;
}
if ( isset( self::STRUCTURAL_BLOCK_NAMES[ $rec['name'] ] ) ) {
if ( isset( $matched_post_parents[ $rec['name'] ] ) ) {
continue;
}
$structural_changes[] = array(
'kind' => 'nest',
'description' => sprintf(
/* translators: %s: block name */
__( 'Removed %s wrapper', 'woocommerce' ),
self::block_label( $rec['name'] )
),
'path' => $rec['path'],
);
continue;
}
$removed_blocks[] = array(
'name' => $rec['name'],
'label' => self::block_label( $rec['name'] ),
'path' => $rec['path'],
);
}//end foreach
// Reorder pass: pair like-named entries between added and removed
// and reclassify them as a `reorder` structural change. LCS only
// matches in-order, so an actual reorder of matched blocks lands here
// as add+remove pairs. Reorder entries omit `path` because they
// describe a structural fact, not a single block.
//
// Pairing keys on the normalized block name (e.g. `core/heading`),
// not the humanized label. Two distinct namespaces — say
// `vendor-a/header` and `vendor-b/header` — both produce the label
// `Header` after `block_label()` strips the namespace; pairing on
// label would falsely emit a single `Reordered Header` entry instead
// of one add + one remove.
$added_name_indices = array();
$removed_name_indices = array();
foreach ( $added_blocks as $i => $entry ) {
$added_name_indices[ (string) $entry['name'] ][] = $i;
}
foreach ( $removed_blocks as $i => $entry ) {
$removed_name_indices[ (string) $entry['name'] ][] = $i;
}
$dropped_added = array();
$dropped_removed = array();
foreach ( $added_name_indices as $name => $a_indices ) {
$r_indices = $removed_name_indices[ $name ] ?? array();
$pairs = (int) min( count( $a_indices ), count( $r_indices ) );
if ( 0 === $pairs ) {
continue;
}
$label = self::block_label( (string) $name );
for ( $i = 0; $i < $pairs; $i++ ) {
$structural_changes[] = array(
'kind' => 'reorder',
'description' => sprintf(
/* translators: %s: block name */
__( 'Reordered %s', 'woocommerce' ),
$label
),
);
$dropped_added[ $a_indices[ $i ] ] = true;
$dropped_removed[ $r_indices[ $i ] ] = true;
}
}//end foreach
$added_blocks = self::reject_indices( $added_blocks, $dropped_added );
$removed_blocks = self::reject_indices( $removed_blocks, $dropped_removed );
return array(
'added_blocks' => $added_blocks,
'removed_blocks' => $removed_blocks,
'copy_changes' => $copy_changes,
'structural_changes' => $structural_changes,
);
}