Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
41 changes: 38 additions & 3 deletions includes/class-static-site-importer-document.php
Original file line number Diff line number Diff line change
Expand Up @@ -228,12 +228,12 @@ private function compute_selection(): array {
$body = $this->first_element( 'body' );
$root = $body instanceof DOMElement ? $body : $this->dom->documentElement;

$footer = $this->first_element( 'footer' );
$main = $this->first_element( 'main' );
$footer = $this->first_separable_landmark( 'footer', $main );
$effective_children = $this->effective_root_children( $root, $main );

$header = $this->first_plausible_global_header( $effective_children );
$nav = $this->first_plausible_global_nav( $effective_children, $header );
$nav = $this->first_plausible_global_nav( $effective_children, $header, $main );
$body_headers = $this->body_content_headers( $effective_children, $header, $nav );

if ( $header instanceof DOMElement && $this->contains_same_node( $body_headers, $header ) ) {
Expand Down Expand Up @@ -425,12 +425,21 @@ private function first_plausible_global_header( array $effective_children ): ?DO
/**
* Find a nav that is plausible reusable site chrome.
*
* Navs embedded inside the page body (`<main>`) are page content, not
* separable global chrome: extracting them duplicates the markup because the
* body keeps its own copy.
*
* @param DOMElement[] $effective_children Effective body-level direct children.
* @param DOMElement|null $header Selected header element.
* @param DOMElement|null $main Main landmark element if present.
* @return DOMElement|null
*/
private function first_plausible_global_nav( array $effective_children, ?DOMElement $header ): ?DOMElement {
private function first_plausible_global_nav( array $effective_children, ?DOMElement $header, ?DOMElement $main = null ): ?DOMElement {
foreach ( $this->dom->getElementsByTagName( 'nav' ) as $nav ) {
if ( $main instanceof DOMElement && $this->is_descendant_of( $nav, $main ) ) {
continue;
}

if ( $header instanceof DOMElement && $this->is_descendant_of( $nav, $header ) ) {
return $nav;
}
Expand All @@ -443,6 +452,32 @@ private function first_plausible_global_nav( array $effective_children, ?DOMElem
return null;
}

/**
* Find the first landmark that is separable from the page body.
*
* Landmarks nested inside `<main>` stay in the page body fragment, so
* extracting them as shared chrome would duplicate their markup.
*
* @param string $tag Landmark tag name.
* @param DOMElement|null $main Main landmark element if present.
* @return DOMElement|null
*/
private function first_separable_landmark( string $tag, ?DOMElement $main ): ?DOMElement {
foreach ( $this->dom->getElementsByTagName( $tag ) as $node ) {
if ( ! $node instanceof DOMElement ) {
continue;
}

if ( $main instanceof DOMElement && $this->is_descendant_of( $node, $main ) ) {
continue;
}

return $node;
}

return null;
}

/**
* Check whether a header looks like global site chrome rather than section content.
*
Expand Down
83 changes: 79 additions & 4 deletions includes/class-static-site-importer-theme-materializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -991,6 +991,7 @@ public static function template_part_artifact_writes( string $theme_dir, array $
*/
private static function source_file_template_parts( array $artifacts ): array {
$files = isset( $artifacts['source_files'] ) && is_array( $artifacts['source_files'] ) ? $artifacts['source_files'] : array();
$files = self::entrypoint_first_source_files( $files, $artifacts );
foreach ( $files as $file ) {
if ( ! is_array( $file ) || ! isset( $file['path'] ) || ! is_scalar( $file['path'] ) ) {
continue;
Expand Down Expand Up @@ -1024,6 +1025,43 @@ private static function source_file_template_parts( array $artifacts ): array {
return array();
}

/**
* Order source files so the site entrypoint is scanned first.
*
* The entrypoint page is the imported front page, so its chrome is the
* authoritative candidate for shared template parts. Without this ordering
* the fallback synthesizes parts from whichever file happens to sort first.
*
* @param array<int,mixed> $files Source files.
* @param array<string,mixed> $artifacts WordPress artifacts from Blocks Engine.
* @return array<int,mixed>
*/
private static function entrypoint_first_source_files( array $files, array $artifacts ): array {
$entrypoint = '';
foreach ( array( 'entry_path', 'entrypoint' ) as $key ) {
if ( isset( $artifacts[ $key ] ) && is_scalar( $artifacts[ $key ] ) && '' !== trim( (string) $artifacts[ $key ] ) ) {
$entrypoint = ltrim( str_replace( '\\', '/', trim( (string) $artifacts[ $key ] ) ), '/' );
break;
}
}
if ( '' === $entrypoint ) {
return $files;
}

$leading = array();
$trailing = array();
foreach ( $files as $file ) {
$path = is_array( $file ) && isset( $file['path'] ) && is_scalar( $file['path'] ) ? ltrim( str_replace( '\\', '/', (string) $file['path'] ), '/' ) : '';
if ( '' !== $path && ( $path === $entrypoint || str_ends_with( $path, '/' . $entrypoint ) ) ) {
$leading[] = $file;
continue;
}
$trailing[] = $file;
}

return array_merge( $leading, $trailing );
}

/**
* Convert source HTML landmarks into reusable block theme parts.
*
Expand Down Expand Up @@ -1079,7 +1117,11 @@ private static function strip_inline_svg_markup( string $html ): string {
}

/**
* Return the first matching landmark's outer HTML.
* Return the first separable landmark's outer HTML.
*
* Landmarks nested inside `<main>` remain part of the page body markup, so
* synthesizing a shared template part from them would render the same chrome
* twice. Only landmarks outside the page body are reusable site chrome.
*
* @param string $html Source HTML document.
* @param string $tag Landmark tag name.
Expand All @@ -1099,9 +1141,42 @@ private static function first_landmark_html( string $html, string $tag ): string
return '';
}

$nodes = $dom->getElementsByTagName( $tag );
$node = $nodes->length > 0 ? $nodes->item( 0 ) : null;
return $node instanceof DOMElement ? trim( (string) $dom->saveHTML( $node ) ) : '';
$mains = $dom->getElementsByTagName( 'main' );
$main = $mains->length > 0 ? $mains->item( 0 ) : null;

foreach ( $dom->getElementsByTagName( $tag ) as $node ) {
if ( ! $node instanceof DOMElement ) {
continue;
}

if ( $main instanceof DOMElement && self::dom_is_descendant_of( $node, $main ) ) {
continue;
}

return trim( (string) $dom->saveHTML( $node ) );
}

return '';
}

/**
* Check whether a DOM node is contained by another element.
*
* @param DOMElement $candidate Candidate element.
* @param DOMElement $ancestor Ancestor element.
* @return bool
*/
private static function dom_is_descendant_of( DOMElement $candidate, DOMElement $ancestor ): bool {
$node = $candidate->parentNode;
while ( $node instanceof DOMNode ) {
if ( $node->isSameNode( $ancestor ) ) {
return true;
}

$node = $node->parentNode;
}

return false;
}

/**
Expand Down
17 changes: 17 additions & 0 deletions includes/class-static-site-importer-transformer-adapter.php
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ private function compiled_result_from_transformer_contract( array $result, array
$artifacts['documents'] = ! empty( $view['documents'] ) && is_array( $view['documents'] ) ? $view['documents'] : ( isset( $result['documents'] ) && is_array( $result['documents'] ) ? $result['documents'] : array() );
$artifacts['files'] = $this->artifact_files_from_site_report( $materialization_plan, ! empty( $view ) ? $view : $result );
$artifacts['source_files'] = $this->source_files_from_artifact( $artifact );
$artifacts['entry_path'] = $this->entry_path_from_artifact( $artifact );
$artifacts['site'] = $materialization_plan;
$artifacts['compiled_site'] = ! empty( $view['compiled_site'] ) && is_array( $view['compiled_site'] ) ? $view['compiled_site'] : array();
$artifacts['template_parts'] = isset( $materialization_plan['template_parts'] ) && is_array( $materialization_plan['template_parts'] ) ? $materialization_plan['template_parts'] : array();
Expand All @@ -116,6 +117,22 @@ private function compiled_result_from_transformer_contract( array $result, array
return $compiled;
}

/**
* Read the declared website entrypoint from an artifact bundle.
*
* @param array<string,mixed> $artifact Website artifact bundle.
* @return string
*/
private function entry_path_from_artifact( array $artifact ): string {
foreach ( array( 'entry_path', 'entrypoint' ) as $key ) {
if ( isset( $artifact[ $key ] ) && is_scalar( $artifact[ $key ] ) && '' !== trim( (string) $artifact[ $key ] ) ) {
return ltrim( str_replace( '\\', '/', trim( (string) $artifact[ $key ] ) ), '/' );
}
}

return '';
}

/**
* Preserve safe source artifact files for downstream WordPress materializers.
*
Expand Down
Loading