@@ -219,11 +219,70 @@ function rewriteHtmlFiles(directory, transform) {
219219 if ( entry . isDirectory ( ) ) {
220220 rewriteHtmlFiles ( entryPath , transform ) ;
221221 } else if ( entry . isFile ( ) && entry . name . endsWith ( ".html" ) ) {
222- fs . writeFileSync ( entryPath , transform ( fs . readFileSync ( entryPath , "utf8" ) ) ) ;
222+ fs . writeFileSync ( entryPath , transform ( fs . readFileSync ( entryPath , "utf8" ) , entryPath ) ) ;
223223 }
224224 }
225225}
226226
227+ function normalizeHtmlHeadAssets ( ) {
228+ rewriteHtmlFiles ( BUILD_DIR , ( html ) => normalizeHtmlHead ( html ) ) ;
229+ }
230+
231+ function normalizeHtmlHead ( html ) {
232+ let normalized = html ;
233+
234+ if ( ! normalized . includes ( 'rel="icon"' ) ) {
235+ normalized = insertBeforeHeadClose ( normalized , ' <link rel="icon" type="image/svg+xml" href="/favicon.svg">\n' ) ;
236+ }
237+
238+ if ( ! normalized . includes ( 'rel="apple-touch-icon"' ) ) {
239+ normalized = insertBeforeHeadClose ( normalized , ' <link rel="apple-touch-icon" href="/apple-touch-icon.png">\n' ) ;
240+ }
241+
242+ if ( ! normalized . includes ( "data-v8s-theme-override" ) ) {
243+ normalized = insertBeforeFirstStylesheet ( normalized , `${ THEME_OVERRIDE_SCRIPT } \n` ) ;
244+ }
245+
246+ return normalized ;
247+ }
248+
249+ function insertBeforeHeadClose ( html , insertion ) {
250+ return html . replace ( / < \/ h e a d > / i, `${ insertion } </head>` ) ;
251+ }
252+
253+ function insertBeforeFirstStylesheet ( html , insertion ) {
254+ if ( / < l i n k \s + [ ^ > ] * r e l = [ " ' ] s t y l e s h e e t [ " ' ] [ ^ > ] * > / i. test ( html ) ) {
255+ return html . replace ( / ( < l i n k \s + [ ^ > ] * r e l = [ " ' ] s t y l e s h e e t [ " ' ] [ ^ > ] * > ) / i, `${ insertion } $1` ) ;
256+ }
257+
258+ return insertBeforeHeadClose ( html , insertion ) ;
259+ }
260+
261+ const THEME_OVERRIDE_SCRIPT = ` <script data-v8s-theme-override>
262+ (() => {
263+ const theme = new URLSearchParams(window.location.search).get("theme");
264+ if (theme !== "light" && theme !== "dark") return;
265+
266+ document.documentElement.dataset.theme = theme;
267+
268+ const applyThemeImages = () => {
269+ if (theme !== "dark") return;
270+
271+ document.querySelectorAll('picture source[media*="prefers-color-scheme"][srcset]').forEach((source) => {
272+ const image = source.parentElement?.querySelector("img");
273+ const candidate = source.getAttribute("srcset")?.split(",")[0]?.trim()?.split(/\\s+/)[0];
274+ if (image && candidate) image.src = candidate;
275+ });
276+ };
277+
278+ if (document.readyState === "loading") {
279+ document.addEventListener("DOMContentLoaded", applyThemeImages, { once: true });
280+ } else {
281+ applyThemeImages();
282+ }
283+ })();
284+ </script>` ;
285+
227286function isDefaultLegalTemplate ( filePath ) {
228287 if ( ! fs . existsSync ( filePath ) ) return false ;
229288 const html = fs . readFileSync ( filePath , "utf8" ) ;
@@ -714,6 +773,7 @@ ${pageLinks}
714773 </ul>
715774 </section>
716775${ language === "en" ? renderMachineReadableTestsSection ( ) : "" }
776+ ${ language === "en" ? renderThemeTestsSection ( ) : "" }
717777 <section class="qa-section">
718778 <h3>${ escapeHtml ( metadata . statusTitle ) } </h3>
719779 <ul class="qa-links">
@@ -750,6 +810,36 @@ ${links}
750810 </section>` ;
751811}
752812
813+ function renderThemeTestsSection ( ) {
814+ const links = [
815+ [ "/" , "Index" ] ,
816+ [ "/expand" , "Expand" ] ,
817+ [ "/404" , "404" ] ,
818+ [ "/expired" , "Expired" ] ,
819+ [ "/disabled" , "Disabled" ] ,
820+ [ "/maintenance" , "Maintenance" ] ,
821+ [ "/_stats/" , "Stats" ]
822+ ]
823+ . flatMap ( ( [ href , label ] ) => [
824+ [ withTheme ( href , "light" ) , `${ label } light` ] ,
825+ [ withTheme ( href , "dark" ) , `${ label } dark` ]
826+ ] )
827+ . map ( ( [ href , label ] ) => renderTestsLink ( href , label ) )
828+ . join ( "\n" ) ;
829+
830+ return ` <section class="qa-section">
831+ <h3>Theme checks</h3>
832+ <ul class="qa-links">
833+ ${ links }
834+ </ul>
835+ </section>` ;
836+ }
837+
838+ function withTheme ( href , theme ) {
839+ const separator = href . includes ( "?" ) ? "&" : "?" ;
840+ return `${ href } ${ separator } theme=${ theme } ` ;
841+ }
842+
753843function encodePathSegment ( value ) {
754844 return encodeURIComponent ( String ( value || "" ) . trim ( ) ) . replace ( / % 2 F / gi, "/" ) ;
755845}
@@ -907,6 +997,7 @@ function main() {
907997 writeRuntimeSiteConfig ( runtimeSiteConfig ( siteConfig ) , RUNTIME_SITE_CONFIG_PATH ) ;
908998 removeDeferredLegalPages ( siteConfig ) ;
909999 buildTestsPage ( siteConfig ) ;
1000+ normalizeHtmlHeadAssets ( ) ;
9101001 copyRuntimeBlocklist ( ) ;
9111002 buildRedirectTargets ( ) ;
9121003 validateRuntimeRegistry ( ) ;
0 commit comments