'index.html', '' => 'index.html', '/hello-world/' => 'hello-world/index.html', '/hello-world' => 'hello-world/index.html', '/blog/2026/09/a-post/' => 'blog/2026/09/a-post/index.html', '/robots.txt' => 'robots.txt', '/wp-content/themes/x/style.css' => 'wp-content/themes/x/style.css', '/sitemap.xml' => 'sitemap.xml', // Ends in a slash, so it is a page whose name happens to contain a dot — // not a file called "about.us". '/about.us/' => 'about.us/index.html', ); foreach ( $cases as $url => $expected ) { T::is( SPE_Mapper::path_for_url_path( $url ), $expected, "'{$url}' maps to '{$expected}'" ); } T::ok( SPE_Mapper::is_asset( 'wp-content/x.css' ), 'a stylesheet is an asset' ); T::ok( SPE_Mapper::is_asset( 'a/b/photo.JPG' ), 'an uppercase extension still reads as an asset' ); T::ok( ! SPE_Mapper::is_asset( 'blog/post/index.html' ), 'a page is not an asset' ); // Distinct pages must never land on one path. $paths = array(); foreach ( array( '/a/', '/b/', '/a/b/', '/a/b/c/', '/a.html', '/ab/' ) as $url ) { $paths[] = SPE_Mapper::path_for_url_path( $url ); } T::is( count( array_unique( $paths ) ), count( $paths ), 'six distinct URLs give six distinct paths' ); // `/a/` and `/a/index.html` are the same page in WordPress and are SUPPOSED to // share a path — this records that as intended, not as the collision above. T::is( SPE_Mapper::path_for_url_path( '/a/' ), SPE_Mapper::path_for_url_path( '/a/index.html' ), 'a directory URL and its explicit index are one page' ); T::group( 'document-relative links' ); $relative = array( // from to expected array( 'index.html', 'about/index.html', 'about/index.html' ), array( 'about/index.html', 'index.html', '../index.html' ), array( 'blog/post/index.html', 'wp-content/app.css', '../../wp-content/app.css' ), array( 'blog/post/index.html', 'blog/other/index.html', '../other/index.html' ), array( 'a/b/c/index.html', 'a/b/c/style.css', 'style.css' ), array( 'index.html', 'index.html', 'index.html' ), ); foreach ( $relative as $case ) { list( $from, $to, $expected ) = $case; T::is( SPE_Renderer::relative( $from, $to ), $expected, "{$from} -> {$to}" ); } T::group( 'rewriting a page' ); $html = '' . '' . '' . '' . 'Home' . 'About' . 'Offsite' . '' . 'Notes' . ''; $result = SPE_Renderer::rewrite( $html, 'blog/hello/index.html' ); $out = $result['html']; T::ok( false === strpos( $out, 'https://example.test' ), 'no absolute self-links remain' ); T::ok( false !== strpos( $out, 'href="../../index.html"' ), 'the home link became document-relative' ); T::ok( false !== strpos( $out, 'href="../../about/index.html"' ), 'an internal page link became relative' ); T::ok( false !== strpos( $out, 'href="https://elsewhere.example/offsite"' ), 'an external link is left alone' ); T::ok( false !== strpos( $out, 'href="../../wp-content/themes/x/style.css"' ), 'the ?ver= query is dropped from an asset, which the bucket stores once' ); T::ok( false !== strpos( $out, 'src="../../wp-content/uploads/pic.png"' ), 'a protocol-relative URL is rewritten too' ); T::ok( false !== strpos( $out, 'href="index.html#notes"' ), 'a fragment survives rewriting' ); T::ok( isset( $result['assets']['wp-content/themes/x/style.css'] ), 'the stylesheet was collected for export' ); T::ok( isset( $result['assets']['wp-content/uploads/pic.png'] ), 'the image was collected for export' ); T::is( $result['assets']['wp-content/themes/x/style.css'], 'https://example.test/wp-content/themes/x/style.css', 'a collected asset carries the URL to fetch it from' ); T::ok( ! isset( $result['assets']['about/index.html'] ), 'a page is not collected as an asset' ); T::group( 'rewriting when the site URL carries a port' ); /* * A local instance, a staging box behind a port, anything not on 80 or 443. * This case is worth its own group because two separate bugs lived here: a * naive protocol-relative pass that matched inside every absolute URL and * gave each one a second scheme, and a host-only match that left the port * stranded as `//host:8760:8760/`. Both produced links that looked almost * right and went nowhere. */ // Swap the stub's idea of where the site is for the length of this group. $GLOBALS['sirius_test_home'] = 'http://127.0.0.1:8760'; $with_port = 'feed' . 'home' . 'about' . ''; $ported_out = SPE_Renderer::rewrite( $with_port, 'index.html' )['html']; T::ok( false === strpos( $ported_out, 'http:http' ), 'no doubled scheme' ); T::ok( false === strpos( $ported_out, ':8760:8760' ), 'no doubled port' ); T::ok( false === strpos( $ported_out, '127.0.0.1' ), 'no absolute self-links remain' ); T::ok( false !== strpos( $ported_out, 'href="index.html?feed=rss2"' ), 'a query survives on the home page' ); T::ok( false !== strpos( $ported_out, 'href="about/index.html"' ), 'an internal link is relative' ); T::ok( false !== strpos( $ported_out, 'src="wp-content/x.png"' ), 'a protocol-relative asset keeps its port stripped cleanly' ); unset( $GLOBALS['sirius_test_home'] ); T::group( 'the export marker' ); // The exporter's own marker must never survive into a published page: every // internal link would carry it, and the published copy would then be asking // for the export-rendered variant of every page. $flagged = 'About'; $rewrote = SPE_Renderer::rewrite( $flagged, 'index.html' )['html']; T::ok( false === strpos( $rewrote, 'sirius_export' ), 'the export marker is stripped' ); exit( T::summary() );