walk( parse_blocks( $content ), array() ); } /** * Recursively walks parsed blocks, collecting controlled ones. * * @param array $blocks Parsed blocks. * @param int[] $trail Index path of the parent block. * @return Occurrence[] */ private function walk( array $blocks, array $trail ): array { $found = array(); // Only blocks the editor shows count towards the index, so paths match // the block positions in its List View. $index = -1; foreach ( $blocks as $block ) { if ( ! is_array( $block ) ) { continue; } $name = isset( $block['blockName'] ) && is_string( $block['blockName'] ) ? $block['blockName'] : ''; /* * The parser emits nameless blocks for the HTML between blocks. The * editor drops whitespace-only chunks but shows any other chunk as a * Classic block, which takes a List View position. */ if ( '' === $name ) { $html = isset( $block['innerHTML'] ) && is_string( $block['innerHTML'] ) ? $block['innerHTML'] : ''; if ( $this->has_visible_text( $html ) ) { ++$index; } continue; } $path = array_merge( $trail, array( ++$index ) ); $model = $this->extract_model( $block ); if ( null !== $model ) { $found[] = new Occurrence( $name, implode( '.', $path ), $model ); } if ( isset( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) && array() !== $block['innerBlocks'] ) { $found = array_merge( $found, $this->walk( $block['innerBlocks'], $path ) ); } } return $found; } /** * Reports whether an HTML chunk is more than whitespace, using the same * notion of whitespace as the editor's JavaScript `trim()` (which also * drops NBSP, the BOM and the Unicode space separators). * * @param string $html HTML chunk. * @return bool */ private function has_visible_text( string $html ): bool { $visible = preg_match( '/[^\s\p{Zs}\x{FEFF}\x{2028}\x{2029}]/u', $html ); // Invalid UTF-8: fall back to ASCII whitespace. return false === $visible ? '' !== trim( $html ) : 1 === $visible; } /** * Extracts a meaningful visibility model from a parsed block. * * Blocks whose model is absent, malformed, or carries neither a flag nor any * conditions are ignored — they are indistinguishable from an untouched block * and would only be noise in the report. * * @param array $block Parsed block. * @return array|null */ private function extract_model( array $block ): ?array { $attrs = isset( $block['attrs'] ) && is_array( $block['attrs'] ) ? $block['attrs'] : array(); $model = $attrs[ self::ATTRIBUTE_KEY ] ?? null; if ( ! is_array( $model ) ) { return null; } $has_conditions = ! empty( $model['conditions'] ) && is_array( $model['conditions'] ); if ( ! $has_conditions && empty( $model['enabled'] ) ) { return null; } return $model; } }