isOpaque()) { return 'opaque:' . GlobalInstance::effectiveName($block); } $presets = NodeSummary::presetIds($block); sort($presets); $children = array_map(self::shape(...), $block->children()); return $block->name() . '|' . (NodeSummary::columnWidth($block) ?? '') . '|' . implode(',', $presets) . '[' . implode(';', $children) . ']'; } private static function body(Block $block): string { if ($block->isOpaque()) { return 'opaque:' . self::text(Serializer::block($block)); } $attrs = $block->attrs(); foreach (self::IGNORED as $key) { unset($attrs[$key]); } if (is_array($attrs['module']['meta'] ?? null)) { unset($attrs['module']['meta']['adminLabel']); } $parts = []; foreach ($block->isVoid() ? [] : $block->parts() as $part) { if ($part instanceof Block) { $parts[] = self::body($part); } elseif (trim($part) !== '') { $parts[] = 'text:' . Json::encode(self::text($part)); } } return $block->name() . Json::encode(self::sorted($attrs)) . '[' . implode(';', $parts) . ']'; } /** Text as visitors get it: line breaks unified, whitespace at both ends dropped. */ private static function text(string $text): string { return trim(str_replace("\r\n", "\n", $text)); } /** Object keys sorted at every depth, so the key order Divi or the plugin wrote does not count. */ private static function sorted(mixed $value): mixed { if ($value instanceof stdClass) { $value = (array) $value; } if (! is_array($value)) { return $value; } if (! array_is_list($value)) { ksort($value); } return array_map(self::sorted(...), $value); } private static function hash(string $value): string { return substr(hash('sha256', $value), 0, 16); } }