fix: identical means identical bytes – text parts count, pages and posts only (ruling Q25)

- Fingerprint::content() includes non-whitespace text parts (raw HTML between
  blocks, inner HTML of core blocks) and opaque blocks with all their bytes;
  structure() unchanged; find-similar "identical" follows
- make-global mode 2 also compares Fingerprint::canonical() bytes of each node
- sources and targets: post types page and post only (POST_TYPES)
- a rolled-back page N>1 keeps DiviSaver's "Revision X bleibt erhalten"
- guide: escaped divi/global-layout re-expanded by Divi's do_blocks in
  shortcode-module and WooCommerce text output documented as residual risk

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01At3V6SjwbMe6eMzVGW3Hs7
This commit is contained in:
2026-09-27 21:59:20 +02:00
co-authored by Claude Opus 5.5
parent e733065b60
commit 4e959833b9
6 changed files with 238 additions and 21 deletions
@@ -68,4 +68,68 @@ final class FingerprintTest extends TestCase
self::assertNotSame(Fingerprint::structure($a), Fingerprint::structure($b));
self::assertNotSame(Fingerprint::structure($a), Fingerprint::structure($c));
}
/** The section of 1055 with $extra inserted at the end of its column (review of Task 15, ruling Q25). */
private static function sectionWith(string $extra): Block
{
$markup = DiviFixtures::markup('stage3/library--library-abschnitt.html');
$at = strrpos($markup, '<!-- /wp:divi/column -->');
$markup = substr($markup, 0, (int) $at) . $extra . substr($markup, (int) $at);
return LibraryContent::roots(LibraryContent::load(new Parser(), $markup))[0];
}
/** @return array<string, array{string, string}> */
public static function visibleExtras(): array
{
return [
'raw html between blocks' => ['<p>ONLY ON B</p>', '<p>ONLY ON C</p>'],
'core paragraph' => [
'<!-- wp:paragraph --><p>ONLY ON B</p><!-- /wp:paragraph -->',
'<!-- wp:paragraph --><p>ONLY ON C</p><!-- /wp:paragraph -->',
],
'divi 4 shortcode text' => ['[et_pb_text]ONLY ON B[/et_pb_text]', '[et_pb_text]ONLY ON C[/et_pb_text]'],
];
}
/**
* Ruling Q25: text parts are visible – raw HTML between blocks and the inner HTML of core blocks count
* for the content, like attributes.
*
* @dataProvider visibleExtras
*/
public function testVisibleTextPartsCountForTheContent(string $extra, string $other): void
{
$plain = self::section();
self::assertNotSame(Fingerprint::content($plain), Fingerprint::content(self::sectionWith($extra)));
self::assertNotSame(
Fingerprint::content(self::sectionWith($extra)),
Fingerprint::content(self::sectionWith($other))
);
self::assertNotSame(Fingerprint::canonical($plain), Fingerprint::canonical(self::sectionWith($extra)));
}
public function testRawTextKeepsTheStructureAndWhitespaceNeverCounts(): void
{
$plain = self::section();
self::assertSame(Fingerprint::structure($plain), Fingerprint::structure(self::sectionWith('<p>x</p>')));
self::assertSame(Fingerprint::content($plain), Fingerprint::content(self::sectionWith("\n \n")));
self::assertSame(Fingerprint::canonical($plain), Fingerprint::canonical(self::sectionWith("\n\t")));
}
/** Opaque blocks count with all their bytes (a global instance's snapshot included). */
public function testOpaqueBlocksCountWithAllTheirBytes(): void
{
$instance = static fn (string $text): Block => LibraryContent::roots(LibraryContent::load(
new Parser(),
'<!-- wp:divi/placeholder --><!-- wp:divi/section --><!-- wp:divi/row --><!-- wp:divi/column -->'
. '<!-- wp:divi/global-layout {"globalModule":"5","blockName":"divi/text"} --><p>' . $text
. '</p><!-- /wp:divi/global-layout -->'
. '<!-- /wp:divi/column --><!-- /wp:divi/row --><!-- /wp:divi/section --><!-- /wp:divi/placeholder -->'
))[0];
self::assertNotSame(Fingerprint::content($instance('a')), Fingerprint::content($instance('b')));
}
}