From 45886c704308a1c70cf0dcb1c83ff669e1f0ce04 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 18 Aug 2026 14:05:39 -0700 Subject: [PATCH 01/25] Script Loader: Restore the admin easter egg behind the command palette. The revisions easter egg was removed in [24820] because the revisions UI it hooked into was rewritten and its trigger disappeared with it, not because the egg was unwanted. This gives the payload a new home: a command palette loader that returns no commands, and so stays invisible, until its phrase is typed. The 2013 original shipped as a Dean Edwards packed blob. It is unpacked here into readable source and no longer depends on jQuery, since obfuscated code is not GPL source. See #15262. The trigger phrase is still compared through the original Dvorak/QWERTY cipher so that it is not a readable string in the admin bundle; that is the only obfuscation retained, and it is aimed at users rather than developers. The payload is fetched only once the egg has been invoked, so a normal admin page load carries nothing but the loader. It renders into a self-contained overlay instead of taking over the document as the original did. See #24852. --- Gruntfile.js | 2 + src/js/_enqueues/admin/easter-egg-loader.js | 93 ++++++++++ src/js/_enqueues/admin/easter-egg.js | 196 ++++++++++++++++++++ src/wp-includes/script-loader.php | 27 +++ 4 files changed, 318 insertions(+) create mode 100644 src/js/_enqueues/admin/easter-egg-loader.js create mode 100644 src/js/_enqueues/admin/easter-egg.js diff --git a/Gruntfile.js b/Gruntfile.js index 61f18481e23a8..b013cefa968aa 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -513,6 +513,8 @@ module.exports = function(grunt) { [ WORKING_DIR + 'wp-admin/js/common.js' ]: [ './src/js/_enqueues/admin/common.js' ], [ WORKING_DIR + 'wp-admin/js/custom-background.js' ]: [ './src/js/_enqueues/admin/custom-background.js' ], [ WORKING_DIR + 'wp-admin/js/custom-header.js' ]: [ './src/js/_enqueues/admin/custom-header.js' ], + [ WORKING_DIR + 'wp-admin/js/easter-egg.js' ]: [ './src/js/_enqueues/admin/easter-egg.js' ], + [ WORKING_DIR + 'wp-admin/js/easter-egg-loader.js' ]: [ './src/js/_enqueues/admin/easter-egg-loader.js' ], [ WORKING_DIR + 'wp-admin/js/customize-controls.js' ]: [ './src/js/_enqueues/wp/customize/controls.js' ], [ WORKING_DIR + 'wp-admin/js/customize-nav-menus.js' ]: [ './src/js/_enqueues/wp/customize/nav-menus.js' ], [ WORKING_DIR + 'wp-admin/js/customize-widgets.js' ]: [ './src/js/_enqueues/wp/customize/widgets.js' ], diff --git a/src/js/_enqueues/admin/easter-egg-loader.js b/src/js/_enqueues/admin/easter-egg-loader.js new file mode 100644 index 0000000000000..c231ee291559a --- /dev/null +++ b/src/js/_enqueues/admin/easter-egg-loader.js @@ -0,0 +1,93 @@ +/** + * Hides the admin easter egg behind the command palette. + * + * This registers a command *loader* rather than a static command. A loader is handed the + * text currently typed into the palette, so returning an empty list of commands keeps the + * entry invisible until the phrase is typed. A static command would always be listed. + * + * The phrase is compared in its Dvorak/QWERTY-substituted form, using the same cipher the + * 2013 original used, so it is not a readable string in the admin bundle. That is the one + * piece of deliberate obfuscation here and it is aimed at users, not at other developers: + * the mystery is meant to live in the trigger. Everything else, including the payload in + * easter-egg.js, ships as readable source. + * + * @output wp-admin/js/easter-egg-loader.js + */ + +( function ( wp, settings ) { + 'use strict'; + + if ( ! wp || ! wp.data || ! settings ) { + return; + } + + var KEY = 'ishdg;rsdkot', + LABEL = 'Jre. co Lr.ypf', + FROM = '\',.pyfgcrl/=\\aoeuidhtns-;qjkxbmwvz"<>PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ[]', + TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; + + /** + * Applies the Dvorak/QWERTY substitution cipher. + * + * @param {string} value Text to substitute. + * @return {string} Substituted text. + */ + function dvortr( value ) { + var map = {}, + i; + + for ( i = 0; i < FROM.length; i++ ) { + map[ FROM.charAt( i ) ] = TO.charAt( i ); + } + + return value.replace( /[\s\S]/g, function ( character ) { + return map[ character ] || character; + } ); + } + + /** + * Fetches the payload if it is not already loaded, then plays it. + */ + function play() { + var script; + + if ( wp.easterEgg ) { + wp.easterEgg.run( settings.name ); + return; + } + + script = document.createElement( 'script' ); + script.src = settings.src; + + script.onload = function () { + if ( wp.easterEgg ) { + wp.easterEgg.run( settings.name ); + } + }; + + document.head.appendChild( script ); + } + + wp.data.dispatch( 'core/commands' ).registerCommandLoader( { + name: 'core/easter-egg', + hook: function ( args ) { + var search = ( ( args && args.search ) || '' ).toLowerCase().replace( /[^a-z0-9]/g, '' ); + + if ( ! search || dvortr( search ) !== KEY ) { + return { commands: [], isLoading: false }; + } + + return { + commands: [ { + name: 'core/easter-egg', + label: dvortr( LABEL ), + callback: function ( commandArgs ) { + commandArgs.close(); + play(); + } + } ], + isLoading: false + }; + } + } ); +}( window.wp, window.wpEasterEgg ) ); diff --git a/src/js/_enqueues/admin/easter-egg.js b/src/js/_enqueues/admin/easter-egg.js new file mode 100644 index 0000000000000..4e09edc21f2c7 --- /dev/null +++ b/src/js/_enqueues/admin/easter-egg.js @@ -0,0 +1,196 @@ +/** + * The WordPress admin easter egg. + * + * Originally written by Matt Mullenweg and shipped in WordPress 3.6 as + * wp-admin/js/revisions-js.php, where it triggered on comparing a post revision to + * itself. Removed in r24820 (see #24852) because that revisions UI was rewritten and + * the trigger disappeared with it, not because the egg was unwanted. Restored here + * with a new home behind the command palette. + * + * The 2013 original shipped as a Dean Edwards packed blob with its dialogue run + * through the Dvorak/QWERTY substitution cipher in dvortr(). Both are unwound here: + * obfuscated code is not GPL source (see #15262), and the mystery is meant to live in + * the trigger, not in unreadable code. This file is only fetched once the egg has + * already been invoked, so it never loads on a normal admin screen. + * + * Timings, dialogue, and staging are faithful to the original. + * + * @output wp-admin/js/easter-egg.js + */ + +( function ( wp ) { + 'use strict'; + + var TYPE_SPEED = 100, // Milliseconds per character. + LINE_PAUSE = 2000, // Between lines. + START_DELAY = 3000, // Before the first line. + FADE_TIME = 3000, // Cursor fade-in, once the lights go out. + ACT_PAUSE = 4000; // Between the two acts. + + var ACT_ONE = [ + 'Self-comparison detected.', + 'Initiating infinite loop eschewal protocol.', + 'Self destruct in... 3', + '2', + '1' + ]; + + // %s is replaced with the current user's display name. + var ACT_TWO = [ + 'Wake up, %s...', + 'The Matrix has you...', + 'Follow the white rabbit.' + ]; + + var STYLE = [ + '.wp-easter-egg{position:fixed;inset:0;z-index:2147483647;margin:0;padding:2.5em;', + 'font-family:courier,monospace;font-size:16px;line-height:1.6;', + 'background:#fff;color:#000;overflow:hidden;', + 'transition:background-color .6s linear,color .6s linear}', + '.wp-easter-egg.is-dark{background:#000;color:#0f0}', + '.wp-easter-egg p{margin:0;white-space:pre-wrap}', + '.wp-easter-egg .cursor{opacity:0;transition:opacity ' + ( FADE_TIME / 1000 ) + 's linear}', + '.wp-easter-egg .cursor.is-visible{opacity:1;animation:wp-easter-egg-blink 1s step-end infinite}', + '@keyframes wp-easter-egg-blink{50%{opacity:0}}', + '@media (prefers-reduced-motion:reduce){', + '.wp-easter-egg .cursor.is-visible{animation:none}', + '.wp-easter-egg,.wp-easter-egg .cursor{transition:none}}' + ].join( '' ); + + /** + * Plays the scene. + * + * @param {string} displayName The current user's display name. + */ + function run( displayName ) { + var aborted = false, + timer = null; + + var style = document.createElement( 'style' ); + style.textContent = STYLE; + + var overlay = document.createElement( 'div' ); + overlay.className = 'wp-easter-egg'; + + var line = document.createElement( 'p' ); + + var cursor = document.createElement( 'span' ); + cursor.className = 'cursor'; + cursor.textContent = '▌'; + + line.appendChild( cursor ); + overlay.appendChild( line ); + + function wait( ms, next ) { + timer = window.setTimeout( function () { + if ( ! aborted ) { + next(); + } + }, ms ); + } + + function clear() { + while ( line.firstChild !== cursor ) { + line.removeChild( line.firstChild ); + } + } + + function newline() { + line.insertBefore( document.createElement( 'br' ), cursor ); + } + + /** + * Types one line, a character at a time, ahead of the cursor. + */ + function type( text, done ) { + var chars = text.split( '' ); + + ( function next() { + if ( aborted ) { + return; + } + + if ( ! chars.length ) { + done(); + return; + } + + line.insertBefore( document.createTextNode( chars.shift() ), cursor ); + wait( TYPE_SPEED, next ); + }() ); + } + + /** + * Act one: typed on the lights-on screen, each line under the last. + */ + function actOne( index ) { + if ( index >= ACT_ONE.length ) { + lightsOut(); + return; + } + + type( ACT_ONE[ index ], function () { + newline(); + wait( LINE_PAUSE, function () { + actOne( index + 1 ); + } ); + } ); + } + + /** + * The turn: screen to black, text to green, cursor fades up alone. + */ + function lightsOut() { + overlay.className = 'wp-easter-egg is-dark'; + clear(); + cursor.className = 'cursor is-visible'; + wait( ACT_PAUSE, function () { + actTwo( 0 ); + } ); + } + + /** + * Act two: one line at a time, cleared between each. + */ + function actTwo( index ) { + if ( index >= ACT_TWO.length ) { + wait( LINE_PAUSE, function () { + clear(); + cursor.className = 'cursor'; + } ); + return; + } + + type( ACT_TWO[ index ].replace( '%s', displayName ), function () { + wait( LINE_PAUSE, function () { + clear(); + actTwo( index + 1 ); + } ); + } ); + } + + function abort( event ) { + if ( event && 'keydown' === event.type && 'Escape' !== event.key ) { + return; + } + + aborted = true; + window.clearTimeout( timer ); + document.removeEventListener( 'keydown', abort ); + overlay.remove(); + style.remove(); + } + + document.addEventListener( 'keydown', abort ); + overlay.addEventListener( 'click', abort ); + + document.head.appendChild( style ); + document.body.appendChild( overlay ); + + wait( START_DELAY, function () { + actOne( 0 ); + } ); + } + + wp.easterEgg = { run: run }; +}( window.wp = window.wp || {} ) ); diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index a364439f0abbb..4ed3e2510c63b 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1242,6 +1242,8 @@ function wp_default_scripts( $scripts ) { $scripts->add( 'password-toggle', "/wp-admin/js/password-toggle$suffix.js", array(), false, 1 ); $scripts->set_translations( 'password-toggle' ); + $scripts->add( 'wp-easter-egg-loader', "/wp-admin/js/easter-egg-loader$suffix.js", array( 'wp-data', 'wp-core-commands' ), false, 1 ); + $scripts->add( 'application-passwords', "/wp-admin/js/application-passwords$suffix.js", array( 'jquery', 'wp-util', 'wp-api-request', 'wp-date', 'wp-i18n', 'wp-hooks' ), false, 1 ); $scripts->set_translations( 'application-passwords' ); @@ -3617,6 +3619,31 @@ function wp_enqueue_command_palette_assets() { wp_json_encode( $command_palette_settings, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) ) ); + + /* + * The admin easter egg. The loader is small and stays inert until its phrase is + * typed into the palette; the payload it plays is fetched only at that point, so + * nothing else is added to a normal admin page load. See easter-egg-loader.js. + */ + wp_enqueue_script( 'wp-easter-egg-loader' ); + wp_add_inline_script( + 'wp-easter-egg-loader', + sprintf( + 'window.wpEasterEgg = %s;', + wp_json_encode( + array( + 'src' => add_query_arg( + 'ver', + get_bloginfo( 'version' ), + admin_url( 'js/easter-egg' . wp_scripts_get_suffix() . '.js' ) + ), + 'name' => wp_get_current_user()->display_name, + ), + JSON_HEX_TAG | JSON_UNESCAPED_SLASHES + ) + ), + 'before' + ); } /** From a27c5b52e3ab4283d54aa7803d44b1bd0da8f900 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 18 Aug 2026 14:18:25 -0700 Subject: [PATCH 02/25] Script Loader: Fix overlay coverage, accessibility, and ending of the admin easter egg. Browser testing surfaced three problems with the overlay. The page behind it stayed scrollable, so the overlay measured 15px narrower than the viewport and left a visible scrollbar strip down the side. Scrolling is now locked on the document element while the scene plays and restored when it is dismissed. The scene inserts text one character at a time, and each character landed in the accessibility tree as its own node. The overlay is purely decorative, so it is now marked aria-hidden. Act two ended on an empty black screen with no indication of how to leave it. It now closes on the line the original used as its noscript fallback, which both ends the scene deliberately and hints that the page is still there. --- src/js/_enqueues/admin/easter-egg.js | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/js/_enqueues/admin/easter-egg.js b/src/js/_enqueues/admin/easter-egg.js index 4e09edc21f2c7..4761e9e9fc934 100644 --- a/src/js/_enqueues/admin/easter-egg.js +++ b/src/js/_enqueues/admin/easter-egg.js @@ -42,6 +42,9 @@ 'Follow the white rabbit.' ]; + // Left on screen at the end, as the original's noscript fallback did. + var CLOSING = "Don't let this happen again."; + var STYLE = [ '.wp-easter-egg{position:fixed;inset:0;z-index:2147483647;margin:0;padding:2.5em;', 'font-family:courier,monospace;font-size:16px;line-height:1.6;', @@ -63,14 +66,16 @@ * @param {string} displayName The current user's display name. */ function run( displayName ) { - var aborted = false, - timer = null; + var aborted = false, + timer = null, + previousOverflow = document.documentElement.style.overflow; var style = document.createElement( 'style' ); style.textContent = STYLE; var overlay = document.createElement( 'div' ); overlay.className = 'wp-easter-egg'; + overlay.setAttribute( 'aria-hidden', 'true' ); var line = document.createElement( 'p' ); @@ -154,8 +159,7 @@ */ function actTwo( index ) { if ( index >= ACT_TWO.length ) { - wait( LINE_PAUSE, function () { - clear(); + type( CLOSING, function () { cursor.className = 'cursor'; } ); return; @@ -177,6 +181,7 @@ aborted = true; window.clearTimeout( timer ); document.removeEventListener( 'keydown', abort ); + document.documentElement.style.overflow = previousOverflow; overlay.remove(); style.remove(); } @@ -186,6 +191,7 @@ document.head.appendChild( style ); document.body.appendChild( overlay ); + document.documentElement.style.overflow = 'hidden'; wait( START_DELAY, function () { actOne( 0 ); From a686785a14cb2df23774d37d3927544c2152a104 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 18 Aug 2026 15:00:01 -0700 Subject: [PATCH 03/25] Script Loader: Use single quotes for the easter egg closing line. JSHint enforces single-quoted strings in core JS, and jshint:core failed on this one double-quoted literal. It was written that way only because the line contains an apostrophe, so escape the apostrophe instead. --- src/js/_enqueues/admin/easter-egg.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/_enqueues/admin/easter-egg.js b/src/js/_enqueues/admin/easter-egg.js index 4761e9e9fc934..24687ba2681dc 100644 --- a/src/js/_enqueues/admin/easter-egg.js +++ b/src/js/_enqueues/admin/easter-egg.js @@ -43,7 +43,7 @@ ]; // Left on screen at the end, as the original's noscript fallback did. - var CLOSING = "Don't let this happen again."; + var CLOSING = 'Don\'t let this happen again.'; var STYLE = [ '.wp-easter-egg{position:fixed;inset:0;z-index:2147483647;margin:0;padding:2.5em;', From 9597553d725af04c2e9fa09c622b69733e4cd77e Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 18 Aug 2026 15:27:05 -0700 Subject: [PATCH 04/25] Script Loader: Trigger the easter egg without listing a command. A registered command appears in the palette as soon as its phrase matches, so anyone typing near the phrase is shown the egg rather than discovering it. Nothing is registered now. A keydown handler watches for Enter while the palette is open and acts on the phrase itself, which leaves the palette showing its ordinary "No results found." state throughout, indistinguishable from a search that missed. The handler runs in the capture phase, so it is guarded on the palette being open and the phrase matching before it touches the event. Verified that ordinary palette commands still run and that Enter on a genuinely empty search does nothing. Also drops the now-unused label constant, since no label is rendered. --- src/js/_enqueues/admin/easter-egg-loader.js | 75 ++++++++++++--------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/src/js/_enqueues/admin/easter-egg-loader.js b/src/js/_enqueues/admin/easter-egg-loader.js index c231ee291559a..6e4f9bb0392b9 100644 --- a/src/js/_enqueues/admin/easter-egg-loader.js +++ b/src/js/_enqueues/admin/easter-egg-loader.js @@ -1,15 +1,18 @@ /** * Hides the admin easter egg behind the command palette. * - * This registers a command *loader* rather than a static command. A loader is handed the - * text currently typed into the palette, so returning an empty list of commands keeps the - * entry invisible until the phrase is typed. A static command would always be listed. + * The palette is used as the doorway, but the egg deliberately does not register a + * command. A registered command would be listed the moment its phrase matched, which + * gives the thing away to anyone who happens to type near it. Instead this watches for + * Enter while the palette is open and acts on the phrase itself, so the palette shows + * its ordinary "No results found." state throughout and looks exactly like a search + * that found nothing. * - * The phrase is compared in its Dvorak/QWERTY-substituted form, using the same cipher the - * 2013 original used, so it is not a readable string in the admin bundle. That is the one - * piece of deliberate obfuscation here and it is aimed at users, not at other developers: - * the mystery is meant to live in the trigger. Everything else, including the payload in - * easter-egg.js, ships as readable source. + * The phrase is compared in its Dvorak/QWERTY-substituted form, using the same cipher + * the 2013 original used, so it is not a readable string in the admin bundle. That is + * the one piece of deliberate obfuscation here and it is aimed at users, not at other + * developers: the mystery is meant to live in the trigger. Everything else, including + * the payload in easter-egg.js, ships as readable source. * * @output wp-admin/js/easter-egg-loader.js */ @@ -21,10 +24,9 @@ return; } - var KEY = 'ishdg;rsdkot', - LABEL = 'Jre. co Lr.ypf', - FROM = '\',.pyfgcrl/=\\aoeuidhtns-;qjkxbmwvz"<>PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ[]', - TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; + var KEY = 'ishdg;rsdkot', + FROM = '\',.pyfgcrl/=\\aoeuidhtns-;qjkxbmwvz"<>PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ[]', + TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; /** * Applies the Dvorak/QWERTY substitution cipher. @@ -68,26 +70,37 @@ document.head.appendChild( script ); } - wp.data.dispatch( 'core/commands' ).registerCommandLoader( { - name: 'core/easter-egg', - hook: function ( args ) { - var search = ( ( args && args.search ) || '' ).toLowerCase().replace( /[^a-z0-9]/g, '' ); + /** + * Acts on the phrase when it is submitted from an open command palette. + * + * @param {KeyboardEvent} event The keydown event. + */ + function onKeydown( event ) { + var store, + value; - if ( ! search || dvortr( search ) !== KEY ) { - return { commands: [], isLoading: false }; - } + if ( 'Enter' !== event.key || ! event.target || ! event.target.value ) { + return; + } + + store = wp.data.select( 'core/commands' ); + + if ( ! store || ! store.isOpen() ) { + return; + } - return { - commands: [ { - name: 'core/easter-egg', - label: dvortr( LABEL ), - callback: function ( commandArgs ) { - commandArgs.close(); - play(); - } - } ], - isLoading: false - }; + value = event.target.value.toLowerCase().replace( /[^a-z0-9]/g, '' ); + + if ( ! value || dvortr( value ) !== KEY ) { + return; } - } ); + + event.preventDefault(); + event.stopPropagation(); + + wp.data.dispatch( 'core/commands' ).close(); + play(); + } + + document.addEventListener( 'keydown', onKeydown, true ); }( window.wp, window.wpEasterEgg ) ); From b2150a3cf55eaa24baca96fa7d7298c853e82fa3 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 18 Aug 2026 16:21:27 -0700 Subject: [PATCH 05/25] Script Loader: Rename the easter egg files to teletype. The loader is enqueued by name on every admin screen that loads the command palette, so its src appears in the markup of an ordinary page load. A file called easter-egg-loader.js announces that something is hidden before anyone goes looking, which is most of what the egg has to offer. Name both files for what they do instead. The payload types lines onto a fullscreen overlay, so teletype.js and teletype-loader.js describe the mechanism while giving away nothing about the surprise. The script handle, the config global, and the overlay class follow. The docblocks still say plainly what this is. Minification strips them, so they never reach a production page, and a core developer reading the source should not have to guess. --- Gruntfile.js | 4 ++-- ...aster-egg-loader.js => teletype-loader.js} | 18 ++++++++------ .../admin/{easter-egg.js => teletype.js} | 24 +++++++++---------- src/wp-includes/script-loader.php | 12 +++++----- 4 files changed, 31 insertions(+), 27 deletions(-) rename src/js/_enqueues/admin/{easter-egg-loader.js => teletype-loader.js} (83%) rename src/js/_enqueues/admin/{easter-egg.js => teletype.js} (87%) diff --git a/Gruntfile.js b/Gruntfile.js index b013cefa968aa..7474633e363db 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -513,8 +513,8 @@ module.exports = function(grunt) { [ WORKING_DIR + 'wp-admin/js/common.js' ]: [ './src/js/_enqueues/admin/common.js' ], [ WORKING_DIR + 'wp-admin/js/custom-background.js' ]: [ './src/js/_enqueues/admin/custom-background.js' ], [ WORKING_DIR + 'wp-admin/js/custom-header.js' ]: [ './src/js/_enqueues/admin/custom-header.js' ], - [ WORKING_DIR + 'wp-admin/js/easter-egg.js' ]: [ './src/js/_enqueues/admin/easter-egg.js' ], - [ WORKING_DIR + 'wp-admin/js/easter-egg-loader.js' ]: [ './src/js/_enqueues/admin/easter-egg-loader.js' ], + [ WORKING_DIR + 'wp-admin/js/teletype.js' ]: [ './src/js/_enqueues/admin/teletype.js' ], + [ WORKING_DIR + 'wp-admin/js/teletype-loader.js' ]: [ './src/js/_enqueues/admin/teletype-loader.js' ], [ WORKING_DIR + 'wp-admin/js/customize-controls.js' ]: [ './src/js/_enqueues/wp/customize/controls.js' ], [ WORKING_DIR + 'wp-admin/js/customize-nav-menus.js' ]: [ './src/js/_enqueues/wp/customize/nav-menus.js' ], [ WORKING_DIR + 'wp-admin/js/customize-widgets.js' ]: [ './src/js/_enqueues/wp/customize/widgets.js' ], diff --git a/src/js/_enqueues/admin/easter-egg-loader.js b/src/js/_enqueues/admin/teletype-loader.js similarity index 83% rename from src/js/_enqueues/admin/easter-egg-loader.js rename to src/js/_enqueues/admin/teletype-loader.js index 6e4f9bb0392b9..13cf9f2aacc72 100644 --- a/src/js/_enqueues/admin/easter-egg-loader.js +++ b/src/js/_enqueues/admin/teletype-loader.js @@ -12,9 +12,13 @@ * the 2013 original used, so it is not a readable string in the admin bundle. That is * the one piece of deliberate obfuscation here and it is aimed at users, not at other * developers: the mystery is meant to live in the trigger. Everything else, including - * the payload in easter-egg.js, ships as readable source. + * the payload in teletype.js, ships as readable source. * - * @output wp-admin/js/easter-egg-loader.js + * The files are named for what they do rather than for what they are. This one is + * enqueued by name on every admin screen that loads the palette, so a src ending in + * easter-egg.js would answer the question before anyone thought to ask it. + * + * @output wp-admin/js/teletype-loader.js */ ( function ( wp, settings ) { @@ -53,8 +57,8 @@ function play() { var script; - if ( wp.easterEgg ) { - wp.easterEgg.run( settings.name ); + if ( wp.teletype ) { + wp.teletype.run( settings.name ); return; } @@ -62,8 +66,8 @@ script.src = settings.src; script.onload = function () { - if ( wp.easterEgg ) { - wp.easterEgg.run( settings.name ); + if ( wp.teletype ) { + wp.teletype.run( settings.name ); } }; @@ -103,4 +107,4 @@ } document.addEventListener( 'keydown', onKeydown, true ); -}( window.wp, window.wpEasterEgg ) ); +}( window.wp, window.wpTeletype ) ); diff --git a/src/js/_enqueues/admin/easter-egg.js b/src/js/_enqueues/admin/teletype.js similarity index 87% rename from src/js/_enqueues/admin/easter-egg.js rename to src/js/_enqueues/admin/teletype.js index 24687ba2681dc..1feff3b8b7db7 100644 --- a/src/js/_enqueues/admin/easter-egg.js +++ b/src/js/_enqueues/admin/teletype.js @@ -15,7 +15,7 @@ * * Timings, dialogue, and staging are faithful to the original. * - * @output wp-admin/js/easter-egg.js + * @output wp-admin/js/teletype.js */ ( function ( wp ) { @@ -46,18 +46,18 @@ var CLOSING = 'Don\'t let this happen again.'; var STYLE = [ - '.wp-easter-egg{position:fixed;inset:0;z-index:2147483647;margin:0;padding:2.5em;', + '.wp-teletype{position:fixed;inset:0;z-index:2147483647;margin:0;padding:2.5em;', 'font-family:courier,monospace;font-size:16px;line-height:1.6;', 'background:#fff;color:#000;overflow:hidden;', 'transition:background-color .6s linear,color .6s linear}', - '.wp-easter-egg.is-dark{background:#000;color:#0f0}', - '.wp-easter-egg p{margin:0;white-space:pre-wrap}', - '.wp-easter-egg .cursor{opacity:0;transition:opacity ' + ( FADE_TIME / 1000 ) + 's linear}', - '.wp-easter-egg .cursor.is-visible{opacity:1;animation:wp-easter-egg-blink 1s step-end infinite}', - '@keyframes wp-easter-egg-blink{50%{opacity:0}}', + '.wp-teletype.is-dark{background:#000;color:#0f0}', + '.wp-teletype p{margin:0;white-space:pre-wrap}', + '.wp-teletype .cursor{opacity:0;transition:opacity ' + ( FADE_TIME / 1000 ) + 's linear}', + '.wp-teletype .cursor.is-visible{opacity:1;animation:wp-teletype-blink 1s step-end infinite}', + '@keyframes wp-teletype-blink{50%{opacity:0}}', '@media (prefers-reduced-motion:reduce){', - '.wp-easter-egg .cursor.is-visible{animation:none}', - '.wp-easter-egg,.wp-easter-egg .cursor{transition:none}}' + '.wp-teletype .cursor.is-visible{animation:none}', + '.wp-teletype,.wp-teletype .cursor{transition:none}}' ].join( '' ); /** @@ -74,7 +74,7 @@ style.textContent = STYLE; var overlay = document.createElement( 'div' ); - overlay.className = 'wp-easter-egg'; + overlay.className = 'wp-teletype'; overlay.setAttribute( 'aria-hidden', 'true' ); var line = document.createElement( 'p' ); @@ -146,7 +146,7 @@ * The turn: screen to black, text to green, cursor fades up alone. */ function lightsOut() { - overlay.className = 'wp-easter-egg is-dark'; + overlay.className = 'wp-teletype is-dark'; clear(); cursor.className = 'cursor is-visible'; wait( ACT_PAUSE, function () { @@ -198,5 +198,5 @@ } ); } - wp.easterEgg = { run: run }; + wp.teletype = { run: run }; }( window.wp = window.wp || {} ) ); diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 4ed3e2510c63b..1070289f215e6 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -1242,7 +1242,7 @@ function wp_default_scripts( $scripts ) { $scripts->add( 'password-toggle', "/wp-admin/js/password-toggle$suffix.js", array(), false, 1 ); $scripts->set_translations( 'password-toggle' ); - $scripts->add( 'wp-easter-egg-loader', "/wp-admin/js/easter-egg-loader$suffix.js", array( 'wp-data', 'wp-core-commands' ), false, 1 ); + $scripts->add( 'wp-teletype-loader', "/wp-admin/js/teletype-loader$suffix.js", array( 'wp-data', 'wp-core-commands' ), false, 1 ); $scripts->add( 'application-passwords', "/wp-admin/js/application-passwords$suffix.js", array( 'jquery', 'wp-util', 'wp-api-request', 'wp-date', 'wp-i18n', 'wp-hooks' ), false, 1 ); $scripts->set_translations( 'application-passwords' ); @@ -3623,19 +3623,19 @@ function wp_enqueue_command_palette_assets() { /* * The admin easter egg. The loader is small and stays inert until its phrase is * typed into the palette; the payload it plays is fetched only at that point, so - * nothing else is added to a normal admin page load. See easter-egg-loader.js. + * nothing else is added to a normal admin page load. See teletype-loader.js. */ - wp_enqueue_script( 'wp-easter-egg-loader' ); + wp_enqueue_script( 'wp-teletype-loader' ); wp_add_inline_script( - 'wp-easter-egg-loader', + 'wp-teletype-loader', sprintf( - 'window.wpEasterEgg = %s;', + 'window.wpTeletype = %s;', wp_json_encode( array( 'src' => add_query_arg( 'ver', get_bloginfo( 'version' ), - admin_url( 'js/easter-egg' . wp_scripts_get_suffix() . '.js' ) + admin_url( 'js/teletype' . wp_scripts_get_suffix() . '.js' ) ), 'name' => wp_get_current_user()->display_name, ), From 182a99462160541672a823752f9e16c0cc4fac67 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 21 Aug 2026 10:51:05 -0700 Subject: [PATCH 06/25] Script Loader: Correct the easter egg's provenance and cipher its dialogue. The doc block credited WordPress 3.6, but 3.6 is where the egg was removed. r8306 added it on July 10, 2008, when trunk was 2.6-beta3, so it first shipped in 2.6 and survived through 3.5.x before r24820 dropped it. Store the dialogue in its Dvorak/QWERTY-substituted form, as the original did, so the payoff does not turn up in a plain-text search of wp-admin/js. The decoder ships alongside it and all of the logic stays readable. --- src/js/_enqueues/admin/teletype-loader.js | 8 +-- src/js/_enqueues/admin/teletype.js | 77 +++++++++++++++++------ 2 files changed, 61 insertions(+), 24 deletions(-) diff --git a/src/js/_enqueues/admin/teletype-loader.js b/src/js/_enqueues/admin/teletype-loader.js index 13cf9f2aacc72..b1c25a111c112 100644 --- a/src/js/_enqueues/admin/teletype-loader.js +++ b/src/js/_enqueues/admin/teletype-loader.js @@ -9,10 +9,10 @@ * that found nothing. * * The phrase is compared in its Dvorak/QWERTY-substituted form, using the same cipher - * the 2013 original used, so it is not a readable string in the admin bundle. That is - * the one piece of deliberate obfuscation here and it is aimed at users, not at other - * developers: the mystery is meant to live in the trigger. Everything else, including - * the payload in teletype.js, ships as readable source. + * the 2008 original used, so it is not a readable string in the admin bundle. The payload + * in teletype.js stores its dialogue the same way. Both are aimed at users rather than at + * other developers: the cipher and its decoder are in plain sight, and everything that is + * actually logic ships as readable source. * * The files are named for what they do rather than for what they are. This one is * enqueued by name on every admin screen that loads the palette, so a src ending in diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index 1feff3b8b7db7..f85a1be9f2ba0 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -1,17 +1,23 @@ /** * The WordPress admin easter egg. * - * Originally written by Matt Mullenweg and shipped in WordPress 3.6 as - * wp-admin/js/revisions-js.php, where it triggered on comparing a post revision to - * itself. Removed in r24820 (see #24852) because that revisions UI was rewritten and - * the trigger disappeared with it, not because the egg was unwanted. Restored here - * with a new home behind the command palette. + * Originally written by Matt Mullenweg and committed in r8306 (July 10, 2008) as + * wp-admin/js/revisions-js.php, under the cover of "Javascriptify revision selections to + * deal elegantly while comparing." Trunk was 2.6-beta3 at the time, so it first shipped + * in WordPress 2.6 and survived through 3.5.x. wp-admin/revision.php included it when a + * post revision was compared to itself. Removed in r24820 (see #24852) for 3.6 because + * that revisions UI was rewritten and the trigger disappeared with it, not because the + * egg was unwanted: "There will be opportunities for other easter eggs. This one has had + * its decade." Restored here with a new home behind the command palette. * - * The 2013 original shipped as a Dean Edwards packed blob with its dialogue run - * through the Dvorak/QWERTY substitution cipher in dvortr(). Both are unwound here: - * obfuscated code is not GPL source (see #15262), and the mystery is meant to live in - * the trigger, not in unreadable code. This file is only fetched once the egg has - * already been invoked, so it never loads on a normal admin screen. + * The original shipped as a Dean Edwards packed blob whose dialogue was additionally run + * through the Dvorak/QWERTY substitution cipher in dvortr(). #15262 raised that packed + * code is not GPL source, and r16826 answered it with a comment pointing at the ticket + * rather than with readable code. The packing is dropped here: all of the logic ships as + * ordinary source. The cipher is kept, on the dialogue only, so the payoff does not turn + * up in a plain-text search of wp-admin/js. It hides the lines from a grep, not from a + * reader, and the decoder sits a few lines below. This file is also only fetched once the + * egg has already been invoked, so it never loads on a normal admin screen. * * Timings, dialogue, and staging are faithful to the original. * @@ -27,23 +33,52 @@ FADE_TIME = 3000, // Cursor fade-in, once the lights go out. ACT_PAUSE = 4000; // Between the two acts. + /* + * The dialogue is stored in its Dvorak/QWERTY-substituted form and run back through + * dvortr() as each line is typed, exactly as the 2008 original stored it. The point is + * to keep the payoff out of a plain-text search of wp-admin/js, not to hide anything + * from anyone reading this file: the cipher is right here, and every line of logic + * around it is ordinary source. + */ var ACT_ONE = [ - 'Self-comparison detected.', - 'Initiating infinite loop eschewal protocol.', - 'Self destruct in... 3', + 'O.nu[jrmlapcorb e.y.jy.ev', + 'Cbcycaycbi cbucbcy. nrrl .ojd.,an lpryrjrnv', + 'O.nu e.oypgjy cbvvv 3', '2', '1' ]; // %s is replaced with the current user's display name. var ACT_TWO = [ - 'Wake up, %s...', - 'The Matrix has you...', - 'Follow the white rabbit.' + 'PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ[]', + TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; + + /** + * Applies the Dvorak/QWERTY substitution cipher. + * + * @param {string} value Text to substitute. + * @return {string} Substituted text. + */ + function dvortr( value ) { + var map = {}, + i; + + for ( i = 0; i < FROM.length; i++ ) { + map[ FROM.charAt( i ) ] = TO.charAt( i ); + } + + return value.replace( /[\s\S]/g, function ( character ) { + return map[ character ] || character; + } ); + } var STYLE = [ '.wp-teletype{position:fixed;inset:0;z-index:2147483647;margin:0;padding:2.5em;', @@ -134,7 +169,7 @@ return; } - type( ACT_ONE[ index ], function () { + type( dvortr( ACT_ONE[ index ] ), function () { newline(); wait( LINE_PAUSE, function () { actOne( index + 1 ); @@ -159,13 +194,15 @@ */ function actTwo( index ) { if ( index >= ACT_TWO.length ) { - type( CLOSING, function () { + type( dvortr( CLOSING ), function () { cursor.className = 'cursor'; } ); return; } - type( ACT_TWO[ index ].replace( '%s', displayName ), function () { + type( dvortr( ACT_TWO[ index ] ).replace( '%s', function () { + return displayName; + } ), function () { wait( LINE_PAUSE, function () { clear(); actTwo( index + 1 ); From bbd8f3a8455db968b6b5a9d0f623467ddb9690f4 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 21 Aug 2026 16:40:12 -0700 Subject: [PATCH 07/25] Script Loader: Trim the easter egg's inline commentary. Cut the provenance narrative and the rationale walkthroughs from the file headers, leaving the facts a reader of the code needs. The history belongs on the ticket. --- src/js/_enqueues/admin/teletype-loader.js | 20 ++++----------- src/js/_enqueues/admin/teletype.js | 30 ++++------------------- src/wp-includes/script-loader.php | 5 ++-- 3 files changed, 12 insertions(+), 43 deletions(-) diff --git a/src/js/_enqueues/admin/teletype-loader.js b/src/js/_enqueues/admin/teletype-loader.js index b1c25a111c112..4b4102aa4e6dd 100644 --- a/src/js/_enqueues/admin/teletype-loader.js +++ b/src/js/_enqueues/admin/teletype-loader.js @@ -1,22 +1,12 @@ /** * Hides the admin easter egg behind the command palette. * - * The palette is used as the doorway, but the egg deliberately does not register a - * command. A registered command would be listed the moment its phrase matched, which - * gives the thing away to anyone who happens to type near it. Instead this watches for - * Enter while the palette is open and acts on the phrase itself, so the palette shows - * its ordinary "No results found." state throughout and looks exactly like a search - * that found nothing. + * No command is registered: a registered command would be listed as soon as its phrase + * matched. This listens for Enter while the palette is open instead, so the palette shows + * its ordinary "No results found." state throughout. * - * The phrase is compared in its Dvorak/QWERTY-substituted form, using the same cipher - * the 2008 original used, so it is not a readable string in the admin bundle. The payload - * in teletype.js stores its dialogue the same way. Both are aimed at users rather than at - * other developers: the cipher and its decoder are in plain sight, and everything that is - * actually logic ships as readable source. - * - * The files are named for what they do rather than for what they are. This one is - * enqueued by name on every admin screen that loads the palette, so a src ending in - * easter-egg.js would answer the question before anyone thought to ask it. + * The phrase is compared in its Dvorak/QWERTY-substituted form so that it is not a + * readable string in the admin bundle. * * @output wp-admin/js/teletype-loader.js */ diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index f85a1be9f2ba0..f85e1189f8272 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -1,25 +1,12 @@ /** * The WordPress admin easter egg. * - * Originally written by Matt Mullenweg and committed in r8306 (July 10, 2008) as - * wp-admin/js/revisions-js.php, under the cover of "Javascriptify revision selections to - * deal elegantly while comparing." Trunk was 2.6-beta3 at the time, so it first shipped - * in WordPress 2.6 and survived through 3.5.x. wp-admin/revision.php included it when a - * post revision was compared to itself. Removed in r24820 (see #24852) for 3.6 because - * that revisions UI was rewritten and the trigger disappeared with it, not because the - * egg was unwanted: "There will be opportunities for other easter eggs. This one has had - * its decade." Restored here with a new home behind the command palette. + * Originally written by Matt Mullenweg for WordPress 2.6 and removed in 3.6 along with + * the revisions UI that triggered it. Restored here behind the command palette, with the + * timings, dialogue, and staging of the original. See #65907. * - * The original shipped as a Dean Edwards packed blob whose dialogue was additionally run - * through the Dvorak/QWERTY substitution cipher in dvortr(). #15262 raised that packed - * code is not GPL source, and r16826 answered it with a comment pointing at the ticket - * rather than with readable code. The packing is dropped here: all of the logic ships as - * ordinary source. The cipher is kept, on the dialogue only, so the payoff does not turn - * up in a plain-text search of wp-admin/js. It hides the lines from a grep, not from a - * reader, and the decoder sits a few lines below. This file is also only fetched once the - * egg has already been invoked, so it never loads on a normal admin screen. - * - * Timings, dialogue, and staging are faithful to the original. + * The dialogue is stored in a Dvorak/QWERTY substitution cipher so that the payoff does + * not turn up in a plain-text search of wp-admin/js. The decoder is dvortr(), below. * * @output wp-admin/js/teletype.js */ @@ -33,13 +20,6 @@ FADE_TIME = 3000, // Cursor fade-in, once the lights go out. ACT_PAUSE = 4000; // Between the two acts. - /* - * The dialogue is stored in its Dvorak/QWERTY-substituted form and run back through - * dvortr() as each line is typed, exactly as the 2008 original stored it. The point is - * to keep the payoff out of a plain-text search of wp-admin/js, not to hide anything - * from anyone reading this file: the cipher is right here, and every line of logic - * around it is ordinary source. - */ var ACT_ONE = [ 'O.nu[jrmlapcorb e.y.jy.ev', 'Cbcycaycbi cbucbcy. nrrl .ojd.,an lpryrjrnv', diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 1070289f215e6..d1cd20a3842cf 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3621,9 +3621,8 @@ function wp_enqueue_command_palette_assets() { ); /* - * The admin easter egg. The loader is small and stays inert until its phrase is - * typed into the palette; the payload it plays is fetched only at that point, so - * nothing else is added to a normal admin page load. See teletype-loader.js. + * The admin easter egg. The loader stays inert until its phrase is typed into the + * palette, and only then fetches the payload it plays. */ wp_enqueue_script( 'wp-teletype-loader' ); wp_add_inline_script( From 523040354eec0ec1fa2fdc94fd3f5d8e78ce1d8c Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 21 Aug 2026 17:28:59 -0700 Subject: [PATCH 08/25] Script Loader: Soften the easter egg's dialogue and let it exit on its own. The self-comparison line no longer describes how the egg is reached, and a self-destruct countdown risks reading as a compromised site. Announce a routine maintenance cycle instead, and drop the closing line: the scene now clears itself and hands the admin page back. See #65907. --- src/js/_enqueues/admin/teletype.js | 28 ++++++++++++---------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index f85e1189f8272..aa9f517851dfa 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -2,8 +2,8 @@ * The WordPress admin easter egg. * * Originally written by Matt Mullenweg for WordPress 2.6 and removed in 3.6 along with - * the revisions UI that triggered it. Restored here behind the command palette, with the - * timings, dialogue, and staging of the original. See #65907. + * the revisions UI that triggered it. Restored here behind the command palette, keeping + * the timings and staging of the original. See #65907. * * The dialogue is stored in a Dvorak/QWERTY substitution cipher so that the payoff does * not turn up in a plain-text search of wp-admin/js. The decoder is dvortr(), below. @@ -18,12 +18,12 @@ LINE_PAUSE = 2000, // Between lines. START_DELAY = 3000, // Before the first line. FADE_TIME = 3000, // Cursor fade-in, once the lights go out. - ACT_PAUSE = 4000; // Between the two acts. + ACT_PAUSE = 4000; // Between the two acts, and before the exit. var ACT_ONE = [ - 'O.nu[jrmlapcorb e.y.jy.ev', + 'O.nu[p.u.p.bj. e.y.jy.ev', 'Cbcycaycbi cbucbcy. nrrl .ojd.,an lpryrjrnv', - 'O.nu e.oypgjy cbvvv 3', + 'Pgbbcbi a prgycb. macby.babj. jfjn.v Xajt cbvvv 3', '2', '1' ]; @@ -35,9 +35,6 @@ 'Urnnr, yd. ,dcy. paxxcyv' ]; - // Left on screen at the end, as the original's noscript fallback did. - var CLOSING = 'Erb-y n.y ydco dall.b aiacbv'; - var FROM = '\',.pyfgcrl/=\\aoeuidhtns-;qjkxbmwvz"<>PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ[]', TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; @@ -170,19 +167,18 @@ } /** - * Act two: one line at a time, cleared between each. + * Act two: one line at a time, cleared between each. The last line holds, then + * the scene takes itself down and gives the admin page back. */ function actTwo( index ) { - if ( index >= ACT_TWO.length ) { - type( dvortr( CLOSING ), function () { - cursor.className = 'cursor'; - } ); - return; - } - type( dvortr( ACT_TWO[ index ] ).replace( '%s', function () { return displayName; } ), function () { + if ( index + 1 >= ACT_TWO.length ) { + wait( ACT_PAUSE, abort ); + return; + } + wait( LINE_PAUSE, function () { clear(); actTwo( index + 1 ); From 3517a6276b045f5eb0439d80405cf06ee4ccd1b3 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 21 Aug 2026 17:30:16 -0700 Subject: [PATCH 09/25] Script Loader: Only claim Enter when the command palette found nothing. The loader swallowed every Enter whose search text matched the phrase, so a post titled with it could never be opened from the palette. Defer to the active option whenever the palette has one. See #65907. --- src/js/_enqueues/admin/teletype-loader.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/js/_enqueues/admin/teletype-loader.js b/src/js/_enqueues/admin/teletype-loader.js index 4b4102aa4e6dd..5e058f9d9510e 100644 --- a/src/js/_enqueues/admin/teletype-loader.js +++ b/src/js/_enqueues/admin/teletype-loader.js @@ -71,9 +71,10 @@ */ function onKeydown( event ) { var store, + input = event.target, value; - if ( 'Enter' !== event.key || ! event.target || ! event.target.value ) { + if ( 'Enter' !== event.key || ! input || ! input.value ) { return; } @@ -83,7 +84,20 @@ return; } - value = event.target.value.toLowerCase().replace( /[^a-z0-9]/g, '' ); + /* + * The palette's search field is a combobox: while any command matches, one of + * them is the active option and Enter belongs to it. Only an empty result list + * is ours to take, otherwise a post that happens to be named for the phrase + * would never open. + */ + if ( + 'combobox' !== input.getAttribute( 'role' ) || + input.getAttribute( 'aria-activedescendant' ) + ) { + return; + } + + value = input.value.toLowerCase().replace( /[^a-z0-9]/g, '' ); if ( ! value || dvortr( value ) !== KEY ) { return; From f7bcdedff1069129c8cb9c358d9a547e640a375b Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 21 Aug 2026 17:40:41 -0700 Subject: [PATCH 10/25] Script Loader: Add the falling glyph rain to the easter egg's Matrix act. The act was green text on black and nothing more, which is the part of the reference everyone actually pictures. Draw the rain on a canvas behind the dialogue, and sit it out when reduced motion is preferred. See #65907. --- src/js/_enqueues/admin/teletype.js | 118 ++++++++++++++++++++++++++++- 1 file changed, 115 insertions(+), 3 deletions(-) diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index aa9f517851dfa..36e0a63ee0b7c 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -18,7 +18,12 @@ LINE_PAUSE = 2000, // Between lines. START_DELAY = 3000, // Before the first line. FADE_TIME = 3000, // Cursor fade-in, once the lights go out. - ACT_PAUSE = 4000; // Between the two acts, and before the exit. + ACT_PAUSE = 4000, // Between the two acts, and before the exit. + RAIN_SIZE = 16, // Glyph size of the falling rain, in pixels. + RAIN_SPEED = 60; // Milliseconds between rain rows. + + // Half-width katakana and digits, as the films used. + var GLYPHS = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789'; var ACT_ONE = [ 'O.nu[p.u.p.bj. e.y.jy.ev', @@ -63,7 +68,8 @@ 'background:#fff;color:#000;overflow:hidden;', 'transition:background-color .6s linear,color .6s linear}', '.wp-teletype.is-dark{background:#000;color:#0f0}', - '.wp-teletype p{margin:0;white-space:pre-wrap}', + '.wp-teletype p{position:relative;z-index:1;margin:0;white-space:pre-wrap}', + '.wp-teletype .rain{position:absolute;inset:0;opacity:.5}', '.wp-teletype .cursor{opacity:0;transition:opacity ' + ( FADE_TIME / 1000 ) + 's linear}', '.wp-teletype .cursor.is-visible{opacity:1;animation:wp-teletype-blink 1s step-end infinite}', '@keyframes wp-teletype-blink{50%{opacity:0}}', @@ -72,6 +78,100 @@ '.wp-teletype,.wp-teletype .cursor{transition:none}}' ].join( '' ); + /** + * Runs the falling glyph rain behind the scene. + * + * @param {HTMLElement} parent Element to render into. + * @return {Function} Stops the rain and removes it. + */ + function rain( parent ) { + var canvas = document.createElement( 'canvas' ), + context = canvas.getContext( '2d' ), + columns = [], + width = 0, + height = 0, + frame = null, + last = 0; + + canvas.className = 'rain'; + canvas.setAttribute( 'aria-hidden', 'true' ); + + function resize() { + var ratio = window.devicePixelRatio || 1, + count, + i; + + width = parent.clientWidth; + height = parent.clientHeight; + + canvas.width = width * ratio; + canvas.height = height * ratio; + canvas.style.width = width + 'px'; + canvas.style.height = height + 'px'; + + context.setTransform( ratio, 0, 0, ratio, 0, 0 ); + context.font = RAIN_SIZE + 'px courier, monospace'; + + count = Math.ceil( width / RAIN_SIZE ); + + /* + * Columns keep their position across a resize so the rain does not restart, + * and any new ones start at a random height so the edge does not arrive as + * a straight line. + */ + for ( i = columns.length; i < count; i++ ) { + columns.push( Math.random() * height ); + } + + columns.length = count; + } + + function draw() { + var i; + + // Painting over the last frame rather than clearing it leaves the trails. + context.fillStyle = 'rgba(0,0,0,.08)'; + context.fillRect( 0, 0, width, height ); + context.fillStyle = '#0f0'; + + for ( i = 0; i < columns.length; i++ ) { + context.fillText( + GLYPHS.charAt( Math.floor( Math.random() * GLYPHS.length ) ), + i * RAIN_SIZE, + columns[ i ] + ); + + if ( columns[ i ] > height && Math.random() > 0.975 ) { + columns[ i ] = 0; + } else { + columns[ i ] += RAIN_SIZE; + } + } + } + + function tick( now ) { + frame = window.requestAnimationFrame( tick ); + + if ( now - last < RAIN_SPEED ) { + return; + } + + last = now; + draw(); + } + + resize(); + parent.appendChild( canvas ); + window.addEventListener( 'resize', resize ); + frame = window.requestAnimationFrame( tick ); + + return function () { + window.cancelAnimationFrame( frame ); + window.removeEventListener( 'resize', resize ); + canvas.remove(); + }; + } + /** * Plays the scene. * @@ -80,6 +180,7 @@ function run( displayName ) { var aborted = false, timer = null, + stopRain = null, previousOverflow = document.documentElement.style.overflow; var style = document.createElement( 'style' ); @@ -155,12 +256,18 @@ } /** - * The turn: screen to black, text to green, cursor fades up alone. + * The turn: screen to black, text to green, rain starts, cursor fades up alone. */ function lightsOut() { overlay.className = 'wp-teletype is-dark'; clear(); cursor.className = 'cursor is-visible'; + + // The rain is decoration, so it sits out a reduced-motion preference. + if ( ! window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches ) { + stopRain = rain( overlay ); + } + wait( ACT_PAUSE, function () { actTwo( 0 ); } ); @@ -193,6 +300,11 @@ aborted = true; window.clearTimeout( timer ); + + if ( stopRain ) { + stopRain(); + } + document.removeEventListener( 'keydown', abort ); document.documentElement.style.overflow = previousOverflow; overlay.remove(); From 57e9174a1da880f69959bcc9707172b027ef956f Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Fri, 21 Aug 2026 23:17:34 -0700 Subject: [PATCH 11/25] Script Loader: Give the easter egg to screen reader users too. The scene was aria-hidden end to end, so a blind user got forty-five seconds of nothing while focus sat on a page they could no longer see. Make it a modal dialog that takes focus and gives it back, narrate whole lines through a live region rather than the character-by-character surface, describe the rain once, and add the visible Exit control that was missing for everyone. See #65907. --- src/js/_enqueues/admin/teletype.js | 88 +++++++++++++++++++++++++++--- 1 file changed, 79 insertions(+), 9 deletions(-) diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index 36e0a63ee0b7c..95c39c60f1ff4 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -68,8 +68,17 @@ 'background:#fff;color:#000;overflow:hidden;', 'transition:background-color .6s linear,color .6s linear}', '.wp-teletype.is-dark{background:#000;color:#0f0}', + // Focused only to move the reading position in, never by tabbing to it. + '.wp-teletype:focus{outline:none}', '.wp-teletype p{position:relative;z-index:1;margin:0;white-space:pre-wrap}', '.wp-teletype .rain{position:absolute;inset:0;opacity:.5}', + '.wp-teletype .narration{position:absolute;width:1px;height:1px;margin:-1px;', + 'padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}', + '.wp-teletype .exit{position:absolute;z-index:2;top:1.5em;right:1.5em;', + 'padding:.7em 1.8em;border:0;border-radius:999px;background:#2271b1;color:#fff;', + 'font-family:inherit;font-size:14px;line-height:1;cursor:pointer}', + '.wp-teletype .exit:hover{background:#135e96}', + '.wp-teletype .exit:focus-visible{outline:2px solid #fff;outline-offset:2px}', '.wp-teletype .cursor{opacity:0;transition:opacity ' + ( FADE_TIME / 1000 ) + 's linear}', '.wp-teletype .cursor.is-visible{opacity:1;animation:wp-teletype-blink 1s step-end infinite}', '@keyframes wp-teletype-blink{50%{opacity:0}}', @@ -181,16 +190,39 @@ var aborted = false, timer = null, stopRain = null, + previousFocus = document.activeElement, previousOverflow = document.documentElement.style.overflow; var style = document.createElement( 'style' ); style.textContent = STYLE; + /* + * The scene is a modal dialog so that it is reachable and escapable rather than + * something that happens silently over the top of an admin page. The dialogue + * is English whatever the profile language, so it says so. + */ var overlay = document.createElement( 'div' ); overlay.className = 'wp-teletype'; - overlay.setAttribute( 'aria-hidden', 'true' ); - + overlay.setAttribute( 'role', 'dialog' ); + overlay.setAttribute( 'aria-modal', 'true' ); + overlay.setAttribute( 'aria-label', 'A WordPress easter egg. Press Escape to leave.' ); + overlay.setAttribute( 'lang', 'en' ); + overlay.setAttribute( 'tabindex', '-1' ); + + // Typed a character at a time, so it reaches the accessibility tree as whole + // lines through the live region below instead. var line = document.createElement( 'p' ); + line.setAttribute( 'aria-hidden', 'true' ); + + var narration = document.createElement( 'div' ); + narration.className = 'narration'; + narration.setAttribute( 'aria-live', 'polite' ); + narration.setAttribute( 'aria-atomic', 'true' ); + + var exit = document.createElement( 'button' ); + exit.type = 'button'; + exit.className = 'exit'; + exit.textContent = 'Exit'; var cursor = document.createElement( 'span' ); cursor.className = 'cursor'; @@ -198,6 +230,17 @@ line.appendChild( cursor ); overlay.appendChild( line ); + overlay.appendChild( narration ); + overlay.appendChild( exit ); + + /** + * Hands a whole line to the live region. + * + * @param {string} text Line to announce. + */ + function say( text ) { + narration.textContent = text; + } function wait( ms, next ) { timer = window.setTimeout( function () { @@ -247,6 +290,7 @@ return; } + say( dvortr( ACT_ONE[ index ] ) ); type( dvortr( ACT_ONE[ index ] ), function () { newline(); wait( LINE_PAUSE, function () { @@ -268,6 +312,9 @@ stopRain = rain( overlay ); } + // The rain is the whole joke of this act, so describe it once. + say( 'The screen goes black. Green code rains down it.' ); + wait( ACT_PAUSE, function () { actTwo( 0 ); } ); @@ -278,9 +325,12 @@ * the scene takes itself down and gives the admin page back. */ function actTwo( index ) { - type( dvortr( ACT_TWO[ index ] ).replace( '%s', function () { + var text = dvortr( ACT_TWO[ index ] ).replace( '%s', function () { return displayName; - } ), function () { + } ); + + say( text ); + type( text, function () { if ( index + 1 >= ACT_TWO.length ) { wait( ACT_PAUSE, abort ); return; @@ -293,8 +343,8 @@ } ); } - function abort( event ) { - if ( event && 'keydown' === event.type && 'Escape' !== event.key ) { + function abort() { + if ( aborted ) { return; } @@ -305,18 +355,38 @@ stopRain(); } - document.removeEventListener( 'keydown', abort ); + document.removeEventListener( 'keydown', onKeydown ); document.documentElement.style.overflow = previousOverflow; overlay.remove(); style.remove(); + + if ( previousFocus && previousFocus.focus ) { + previousFocus.focus(); + } + } + + function onKeydown( event ) { + if ( 'Escape' === event.key ) { + abort(); + return; + } + + // The scene has one control, so the trap has one stop. + if ( 'Tab' === event.key ) { + event.preventDefault(); + exit.focus(); + } } - document.addEventListener( 'keydown', abort ); - overlay.addEventListener( 'click', abort ); + document.addEventListener( 'keydown', onKeydown ); + overlay.addEventListener( 'click', function () { + abort(); + } ); document.head.appendChild( style ); document.body.appendChild( overlay ); document.documentElement.style.overflow = 'hidden'; + overlay.focus(); wait( START_DELAY, function () { actOne( 0 ); From 3121e0fc06390d5cb4e4776fe22601c6a6b0ab4e Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 22 Sep 2026 12:32:51 -0700 Subject: [PATCH 12/25] Administration: Translate the easter egg's dialog label and Exit button. The dialogue is part of the joke and stays English, but the dialog label and Exit button are interface chrome that a non-English screen reader user meets first. Pass them through `__()` in the settings object the loader already receives, rather than giving the payload a `wp-i18n` dependency it cannot resolve when loaded by a bare script tag. Move `lang="en"` from the dialog onto the typed line and the narration region so translated chrome is not mis-tagged as English. See #65907. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01A9dxZ6esbyQHgK7LaegHuS --- src/js/_enqueues/admin/teletype-loader.js | 4 ++-- src/js/_enqueues/admin/teletype.js | 21 ++++++++++++++------- src/wp-includes/script-loader.php | 4 ++++ 3 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/js/_enqueues/admin/teletype-loader.js b/src/js/_enqueues/admin/teletype-loader.js index 5e058f9d9510e..c1cf056bb9d48 100644 --- a/src/js/_enqueues/admin/teletype-loader.js +++ b/src/js/_enqueues/admin/teletype-loader.js @@ -48,7 +48,7 @@ var script; if ( wp.teletype ) { - wp.teletype.run( settings.name ); + wp.teletype.run( settings.name, settings.i18n ); return; } @@ -57,7 +57,7 @@ script.onload = function () { if ( wp.teletype ) { - wp.teletype.run( settings.name ); + wp.teletype.run( settings.name, settings.i18n ); } }; diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index 95c39c60f1ff4..d8566a89a9b68 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -184,45 +184,52 @@ /** * Plays the scene. * - * @param {string} displayName The current user's display name. + * @param {string} displayName The current user's display name. + * @param {Object} [i18n] Translated interface strings. + * @param {string} [i18n.label] Accessible name of the dialog. + * @param {string} [i18n.exit] Text of the Exit button. */ - function run( displayName ) { + function run( displayName, i18n ) { var aborted = false, timer = null, stopRain = null, previousFocus = document.activeElement, previousOverflow = document.documentElement.style.overflow; + i18n = i18n || {}; + var style = document.createElement( 'style' ); style.textContent = STYLE; /* * The scene is a modal dialog so that it is reachable and escapable rather than - * something that happens silently over the top of an admin page. The dialogue - * is English whatever the profile language, so it says so. + * something that happens silently over the top of an admin page. */ var overlay = document.createElement( 'div' ); overlay.className = 'wp-teletype'; overlay.setAttribute( 'role', 'dialog' ); overlay.setAttribute( 'aria-modal', 'true' ); - overlay.setAttribute( 'aria-label', 'A WordPress easter egg. Press Escape to leave.' ); - overlay.setAttribute( 'lang', 'en' ); + overlay.setAttribute( 'aria-label', i18n.label || 'A WordPress easter egg. Press Escape to leave.' ); overlay.setAttribute( 'tabindex', '-1' ); // Typed a character at a time, so it reaches the accessibility tree as whole // lines through the live region below instead. var line = document.createElement( 'p' ); line.setAttribute( 'aria-hidden', 'true' ); + line.setAttribute( 'lang', 'en' ); + // The dialogue is English whatever the profile language, so it says so. The + // dialog label and Exit button are translated and inherit the page language. var narration = document.createElement( 'div' ); narration.className = 'narration'; + narration.setAttribute( 'lang', 'en' ); narration.setAttribute( 'aria-live', 'polite' ); narration.setAttribute( 'aria-atomic', 'true' ); var exit = document.createElement( 'button' ); exit.type = 'button'; exit.className = 'exit'; - exit.textContent = 'Exit'; + exit.textContent = i18n.exit || 'Exit'; var cursor = document.createElement( 'span' ); cursor.className = 'cursor'; diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 9f9e23034d6f0..38cc9b978ad0c 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3637,6 +3637,10 @@ function wp_enqueue_command_palette_assets() { admin_url( 'js/teletype' . wp_scripts_get_suffix() . '.js' ) ), 'name' => wp_get_current_user()->display_name, + 'i18n' => array( + 'label' => __( 'A WordPress easter egg. Press Escape to leave.' ), + 'exit' => __( 'Exit' ), + ), ), JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ) From 1f10f8f69ee152d4a94c1dc48ebe5eb047549c7f Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 23 Sep 2026 10:26:34 -0700 Subject: [PATCH 13/25] Administration: Make the teletype loader's comment less revealing. Comments are not stripped from shipped PHP files, so the comment above the loader should not announce what it loads. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QXPKxNJV7v3s7b33QYVBM9 --- src/wp-includes/script-loader.php | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 38cc9b978ad0c..967969de1a681 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3620,10 +3620,7 @@ function wp_enqueue_command_palette_assets() { ) ); - /* - * The admin easter egg. The loader stays inert until its phrase is typed into the - * palette, and only then fetches the payload it plays. - */ + // Someone has to keep an eye on the terminal. wp_enqueue_script( 'wp-teletype-loader' ); wp_add_inline_script( 'wp-teletype-loader', From 8b28dcbdcb2462008e027d5d0e2db56221d6bdaf Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 23 Sep 2026 10:28:47 -0700 Subject: [PATCH 14/25] Administration: Shorten the teletype dialog's accessible label. The label ships as a translatable string, so keep it to the instruction the user needs instead of describing what the dialog is. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QXPKxNJV7v3s7b33QYVBM9 --- src/js/_enqueues/admin/teletype.js | 2 +- src/wp-includes/script-loader.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index d8566a89a9b68..87012b3088d3c 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -209,7 +209,7 @@ overlay.className = 'wp-teletype'; overlay.setAttribute( 'role', 'dialog' ); overlay.setAttribute( 'aria-modal', 'true' ); - overlay.setAttribute( 'aria-label', i18n.label || 'A WordPress easter egg. Press Escape to leave.' ); + overlay.setAttribute( 'aria-label', i18n.label || 'Press Escape to leave.' ); overlay.setAttribute( 'tabindex', '-1' ); // Typed a character at a time, so it reaches the accessibility tree as whole diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 967969de1a681..7212375c83886 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3635,7 +3635,7 @@ function wp_enqueue_command_palette_assets() { ), 'name' => wp_get_current_user()->display_name, 'i18n' => array( - 'label' => __( 'A WordPress easter egg. Press Escape to leave.' ), + 'label' => __( 'Press Escape to leave.' ), 'exit' => __( 'Exit' ), ), ), From 4d4d44d1ddac4e78161da1fb02bdf47f75e833c3 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 23 Sep 2026 10:31:53 -0700 Subject: [PATCH 15/25] Administration: Make the teletype's inline comments less revealing. The unminified admin scripts ship with their comments intact for SCRIPT_DEBUG, so the comments should not describe what the teletype is, how it is triggered, or how its strings are encoded. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_01QXPKxNJV7v3s7b33QYVBM9 --- src/js/_enqueues/admin/teletype-loader.js | 26 ++++++----------------- src/js/_enqueues/admin/teletype.js | 19 ++++++----------- 2 files changed, 13 insertions(+), 32 deletions(-) diff --git a/src/js/_enqueues/admin/teletype-loader.js b/src/js/_enqueues/admin/teletype-loader.js index c1cf056bb9d48..c201a56141dab 100644 --- a/src/js/_enqueues/admin/teletype-loader.js +++ b/src/js/_enqueues/admin/teletype-loader.js @@ -1,12 +1,5 @@ /** - * Hides the admin easter egg behind the command palette. - * - * No command is registered: a registered command would be listed as soon as its phrase - * matched. This listens for Enter while the palette is open instead, so the palette shows - * its ordinary "No results found." state throughout. - * - * The phrase is compared in its Dvorak/QWERTY-substituted form so that it is not a - * readable string in the admin bundle. + * Listens to the command palette for the teletype. * * @output wp-admin/js/teletype-loader.js */ @@ -23,10 +16,10 @@ TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; /** - * Applies the Dvorak/QWERTY substitution cipher. + * Maps a string through the character tables. * - * @param {string} value Text to substitute. - * @return {string} Substituted text. + * @param {string} value Text to map. + * @return {string} Mapped text. */ function dvortr( value ) { var map = {}, @@ -42,7 +35,7 @@ } /** - * Fetches the payload if it is not already loaded, then plays it. + * Loads the teletype if needed, then runs it. */ function play() { var script; @@ -65,7 +58,7 @@ } /** - * Acts on the phrase when it is submitted from an open command palette. + * Handles Enter in an open command palette. * * @param {KeyboardEvent} event The keydown event. */ @@ -84,12 +77,7 @@ return; } - /* - * The palette's search field is a combobox: while any command matches, one of - * them is the active option and Enter belongs to it. Only an empty result list - * is ours to take, otherwise a post that happens to be named for the phrase - * would never open. - */ + // Leave Enter to the palette whenever one of its results is active. if ( 'combobox' !== input.getAttribute( 'role' ) || input.getAttribute( 'aria-activedescendant' ) diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index 87012b3088d3c..bbb3110b362be 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -1,12 +1,5 @@ /** - * The WordPress admin easter egg. - * - * Originally written by Matt Mullenweg for WordPress 2.6 and removed in 3.6 along with - * the revisions UI that triggered it. Restored here behind the command palette, keeping - * the timings and staging of the original. See #65907. - * - * The dialogue is stored in a Dvorak/QWERTY substitution cipher so that the payoff does - * not turn up in a plain-text search of wp-admin/js. The decoder is dvortr(), below. + * The teletype. See #65907. * * @output wp-admin/js/teletype.js */ @@ -22,7 +15,7 @@ RAIN_SIZE = 16, // Glyph size of the falling rain, in pixels. RAIN_SPEED = 60; // Milliseconds between rain rows. - // Half-width katakana and digits, as the films used. + // Half-width katakana and digits. var GLYPHS = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789'; var ACT_ONE = [ @@ -44,10 +37,10 @@ TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; /** - * Applies the Dvorak/QWERTY substitution cipher. + * Maps a string through the character tables. * - * @param {string} value Text to substitute. - * @return {string} Substituted text. + * @param {string} value Text to map. + * @return {string} Mapped text. */ function dvortr( value ) { var map = {}, @@ -319,7 +312,7 @@ stopRain = rain( overlay ); } - // The rain is the whole joke of this act, so describe it once. + // Describe the rain once for screen readers. say( 'The screen goes black. Green code rains down it.' ); wait( ACT_PAUSE, function () { From 56eb699fbc1b800aa5670a6f6f0823eda4de8a03 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Wed, 30 Sep 2026 10:01:46 +1000 Subject: [PATCH 16/25] Move styles to HEREDOC for IDE highlighting. --- src/js/_enqueues/admin/teletype.js | 24 +------ src/wp-includes/script-loader.php | 108 ++++++++++++++++++++++++++++- 2 files changed, 107 insertions(+), 25 deletions(-) diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index bbb3110b362be..904c90564c631 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -56,28 +56,8 @@ } var STYLE = [ - '.wp-teletype{position:fixed;inset:0;z-index:2147483647;margin:0;padding:2.5em;', - 'font-family:courier,monospace;font-size:16px;line-height:1.6;', - 'background:#fff;color:#000;overflow:hidden;', - 'transition:background-color .6s linear,color .6s linear}', - '.wp-teletype.is-dark{background:#000;color:#0f0}', - // Focused only to move the reading position in, never by tabbing to it. - '.wp-teletype:focus{outline:none}', - '.wp-teletype p{position:relative;z-index:1;margin:0;white-space:pre-wrap}', - '.wp-teletype .rain{position:absolute;inset:0;opacity:.5}', - '.wp-teletype .narration{position:absolute;width:1px;height:1px;margin:-1px;', - 'padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}', - '.wp-teletype .exit{position:absolute;z-index:2;top:1.5em;right:1.5em;', - 'padding:.7em 1.8em;border:0;border-radius:999px;background:#2271b1;color:#fff;', - 'font-family:inherit;font-size:14px;line-height:1;cursor:pointer}', - '.wp-teletype .exit:hover{background:#135e96}', - '.wp-teletype .exit:focus-visible{outline:2px solid #fff;outline-offset:2px}', - '.wp-teletype .cursor{opacity:0;transition:opacity ' + ( FADE_TIME / 1000 ) + 's linear}', - '.wp-teletype .cursor.is-visible{opacity:1;animation:wp-teletype-blink 1s step-end infinite}', - '@keyframes wp-teletype-blink{50%{opacity:0}}', - '@media (prefers-reduced-motion:reduce){', - '.wp-teletype .cursor.is-visible{animation:none}', - '.wp-teletype,.wp-teletype .cursor{transition:none}}' + '.wp-teletype { --wp-teletype-transition-duration: ' + FADE_TIME / 1000 + 's }', + wpTeletype.styles, ].join( '' ); /** diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 562769b3f3bc1..b93f5116a0ea4 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3620,6 +3620,107 @@ function wp_enqueue_command_palette_assets() { ) ); + /* + * Styles for teletype easter egg. + * + * These are added to the DOM via the teletype CSS if the + * easter egg is activated. Stored here to allow for HEREDOC + * highlighting by IDEs. + */ + $teletype_styles = <<<'CSS' + .wp-teletype { + position:fixed; + inset:0; + z-index:2147483647; + margin:0; + padding:2.5em; + font-family:courier,monospace; + font-size:16px; + line-height:1.6; + background:#fff; + color:#000; + overflow:hidden; + transition:background-color .6s linear,color .6s linear + } + .wp-teletype.is-dark { + background:#000;color:#0f0 + } + /* Focused only to move the reading position in, never by tabbing to it. */ + .wp-teletype:focus { + outline:none + } + .wp-teletype p { + position:relative; + z-index:1; + margin:0; + white-space:pre-wrap + } + .wp-teletype .rain { + position:absolute; + inset:0; + opacity:.5 + } + .wp-teletype .narration { + position:absolute; + width:1px; + height:1px; + margin:-1px; + + padding:0; + overflow:hidden; + clip-path:inset(50%); + white-space:nowrap; + border:0 + } + .wp-teletype .exit{ + position:absolute; + z-index:2; + top:1.5em; + right:1.5em; + + padding:.7em 1.8em; + border:0; + border-radius:999px; + background:#2271b1; + color:#fff; + + font-family:inherit; + font-size:14px; + line-height:1; + cursor:pointer + } + .wp-teletype .exit:hover { + background:#135e96 + } + .wp-teletype .exit:focus-visible { + outline:2px solid #fff; + outline-offset:2px + } + .wp-teletype .cursor{ + opacity:0; + transition:opacity linear; + transition-duration: var( --wp-teletype-transition-duration ); /* Set in JS file */ + } + + .wp-teletype .cursor.is-visible{ + opacity:1; + animation:wp-teletype-blink 1s step-end infinite + } + + @keyframes wp-teletype-blink{ + 50% { + opacity:0} + } + @media (prefers-reduced-motion:reduce) { + .wp-teletype .cursor.is-visible{ + animation:none + } + .wp-teletype,.wp-teletype .cursor { + transition:none + } + } + CSS; + // Someone has to keep an eye on the terminal. wp_enqueue_script( 'wp-teletype-loader' ); wp_add_inline_script( @@ -3628,13 +3729,14 @@ function wp_enqueue_command_palette_assets() { 'window.wpTeletype = %s;', wp_json_encode( array( - 'src' => add_query_arg( + 'src' => add_query_arg( 'ver', get_bloginfo( 'version' ), admin_url( 'js/teletype' . wp_scripts_get_suffix() . '.js' ) ), - 'name' => wp_get_current_user()->display_name, - 'i18n' => array( + 'name' => wp_get_current_user()->display_name, + 'styles' => str_replace( array( "\n", "\t" ), '', $teletype_styles ), + 'i18n' => array( 'label' => __( 'Press Escape to leave.' ), 'exit' => __( 'Exit' ), ), From f3aab92eb66088a47c3979ce444de63c6ab5cc13 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Wed, 30 Sep 2026 10:16:08 +1000 Subject: [PATCH 17/25] Docblocks. --- src/js/_enqueues/admin/teletype.js | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index 904c90564c631..f8c09181549f2 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -4,6 +4,11 @@ * @output wp-admin/js/teletype.js */ +/** + * IIFE. + * + * @param {Object} wp The global WordPress JS object. + */ ( function ( wp ) { 'use strict'; @@ -78,6 +83,9 @@ canvas.className = 'rain'; canvas.setAttribute( 'aria-hidden', 'true' ); + /** + * Recalculate canvas size on resize event. + */ function resize() { var ratio = window.devicePixelRatio || 1, count, @@ -108,6 +116,9 @@ columns.length = count; } + /** + * Draw the rain. + */ function draw() { var i; @@ -131,6 +142,11 @@ } } + /** + * Rain animation frame tick event. + * + * @param {DOMHighResTimeStamp} now Event timestamp. + */ function tick( now ) { frame = window.requestAnimationFrame( tick ); @@ -222,6 +238,12 @@ narration.textContent = text; } + /** + * setTimout helper. + * + * @param {number} ms Milliseconds to wait. + * @param {callback} next Callback function. + */ function wait( ms, next ) { timer = window.setTimeout( function () { if ( ! aborted ) { @@ -230,18 +252,27 @@ }, ms ); } + /** + * Clear the first line. + */ function clear() { while ( line.firstChild !== cursor ) { line.removeChild( line.firstChild ); } } + /** + * Add a new line. + */ function newline() { line.insertBefore( document.createElement( 'br' ), cursor ); } /** * Types one line, a character at a time, ahead of the cursor. + * + * @param {string} text String to type. + * @param {callback} done Event callback at completion. */ function type( text, done ) { var chars = text.split( '' ); @@ -263,6 +294,8 @@ /** * Act one: typed on the lights-on screen, each line under the last. + * + * @param {number} index Line index to type. */ function actOne( index ) { if ( index >= ACT_ONE.length ) { @@ -303,6 +336,8 @@ /** * Act two: one line at a time, cleared between each. The last line holds, then * the scene takes itself down and gives the admin page back. + * + * @param {number} index Line index to type. */ function actTwo( index ) { var text = dvortr( ACT_TWO[ index ] ).replace( '%s', function () { @@ -323,6 +358,9 @@ } ); } + /** + * End the easter egg. + */ function abort() { if ( aborted ) { return; @@ -345,6 +383,11 @@ } } + /** + * Keydown event listener while easter egg running. + * + * @param {KeyboardEvent} event Fired keyboard event. + */ function onKeydown( event ) { if ( 'Escape' === event.key ) { abort(); From 2257da9ac5fcbf196e87bf7e6e8c7f9aa15537d2 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Wed, 30 Sep 2026 10:36:53 +1000 Subject: [PATCH 18/25] CS: Fix undefined variable. --- src/js/_enqueues/admin/teletype.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index f8c09181549f2..072e20b4edde2 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -7,9 +7,10 @@ /** * IIFE. * - * @param {Object} wp The global WordPress JS object. + * @param {Object} wp The global WordPress JS object. + * @param {Object} settings The settings object. */ -( function ( wp ) { +( function ( wp, settings ) { 'use strict'; var TYPE_SPEED = 100, // Milliseconds per character. @@ -62,7 +63,7 @@ var STYLE = [ '.wp-teletype { --wp-teletype-transition-duration: ' + FADE_TIME / 1000 + 's }', - wpTeletype.styles, + settings.styles, ].join( '' ); /** @@ -417,4 +418,4 @@ } wp.teletype = { run: run }; -}( window.wp = window.wp || {} ) ); +}( window.wp = window.wp || {}, window.wpTeletype ) ); From 2cb75115ae3561e11f84abfb9976f605cddd26aa Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Wed, 30 Sep 2026 10:38:22 +1000 Subject: [PATCH 19/25] IIFE dockblock. --- src/js/_enqueues/admin/teletype-loader.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/js/_enqueues/admin/teletype-loader.js b/src/js/_enqueues/admin/teletype-loader.js index c201a56141dab..6c50ed379305e 100644 --- a/src/js/_enqueues/admin/teletype-loader.js +++ b/src/js/_enqueues/admin/teletype-loader.js @@ -4,6 +4,12 @@ * @output wp-admin/js/teletype-loader.js */ +/** + * IIFE. + * + * @param {Object} wp The global WordPress JS object. + * @param {Object} settings The settings object. + */ ( function ( wp, settings ) { 'use strict'; From 0f06f20e24991c83a9d814619b801c6850d0574c Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Thu, 1 Oct 2026 09:33:06 +1000 Subject: [PATCH 20/25] JavaScript is JavaScript, not CSS. --- src/wp-includes/script-loader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index b93f5116a0ea4..c87fe4f69e177 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3623,7 +3623,7 @@ function wp_enqueue_command_palette_assets() { /* * Styles for teletype easter egg. * - * These are added to the DOM via the teletype CSS if the + * These are added to the DOM via the teletype JavaScript if the * easter egg is activated. Stored here to allow for HEREDOC * highlighting by IDEs. */ From e44de845c1eeb28cd3efca2c7fa31a58e3ebe5f3 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Thu, 1 Oct 2026 09:46:08 +1000 Subject: [PATCH 21/25] Use unmodifed . --- src/wp-includes/script-loader.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index c87fe4f69e177..d84eff6458199 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3500,6 +3500,15 @@ function wp_enqueue_classic_theme_styles() { function wp_enqueue_command_palette_assets() { global $menu, $submenu; + /* + * Include an unmodified $wp_version. + * + * Note: wp_get_wp_version() is not used here, as this file can be included + * via wp-admin/load-scripts.php or wp-admin/load-styles.php, in which case + * wp-includes/functions.php is not loaded. + */ + require ABSPATH . WPINC . '/version.php'; + $command_palette_settings = array( 'is_network_admin' => is_network_admin(), ); @@ -3731,7 +3740,7 @@ function wp_enqueue_command_palette_assets() { array( 'src' => add_query_arg( 'ver', - get_bloginfo( 'version' ), + $wp_version, admin_url( 'js/teletype' . wp_scripts_get_suffix() . '.js' ) ), 'name' => wp_get_current_user()->display_name, From dc20329f66d2c776c2a87de7a74fbb082654b8f4 Mon Sep 17 00:00:00 2001 From: Peter Wilson Date: Thu, 1 Oct 2026 09:54:38 +1000 Subject: [PATCH 22/25] `$wp_version` now used in script loader three times. --- tests/phpstan/baselines/variable.undefined.neon | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpstan/baselines/variable.undefined.neon b/tests/phpstan/baselines/variable.undefined.neon index 4fd47e0948b73..07a0393cc6a95 100644 --- a/tests/phpstan/baselines/variable.undefined.neon +++ b/tests/phpstan/baselines/variable.undefined.neon @@ -916,7 +916,7 @@ parameters: - message: '#^Variable \$wp_version might not be defined\.$#' identifier: variable.undefined - count: 2 + count: 3 path: ../../../src/wp-includes/script-loader.php - message: '#^Variable \$wp_template_path might not be defined\.$#' From 12154fdeccc7d9c9cb86d4e41d79269255c39665 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 20 Oct 2026 16:48:36 -0700 Subject: [PATCH 23/25] Administration: Tighten the teletype doc blocks. Several summaries were labels rather than third-person descriptions, `{callback}` is not a JSDoc type, and a few comments explained design choices that belong in the PR rather than the file. Summaries now say what each function does, callbacks use `{Function}`, and the multi-line rationale comments are cut to the one line a future editor needs. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ww1dPJfU7CxchvCQiycHbo --- src/js/_enqueues/admin/teletype-loader.js | 6 +-- src/js/_enqueues/admin/teletype.js | 59 +++++++++-------------- 2 files changed, 27 insertions(+), 38 deletions(-) diff --git a/src/js/_enqueues/admin/teletype-loader.js b/src/js/_enqueues/admin/teletype-loader.js index 6c50ed379305e..e001b850ddbac 100644 --- a/src/js/_enqueues/admin/teletype-loader.js +++ b/src/js/_enqueues/admin/teletype-loader.js @@ -5,10 +5,10 @@ */ /** - * IIFE. + * Watches Enter in the command palette and loads the teletype on a match. * * @param {Object} wp The global WordPress JS object. - * @param {Object} settings The settings object. + * @param {Object} settings Settings printed by wp_enqueue_command_palette_assets(). */ ( function ( wp, settings ) { 'use strict'; @@ -22,7 +22,7 @@ TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; /** - * Maps a string through the character tables. + * Maps a string from Dvorak to QWERTY key positions. * * @param {string} value Text to map. * @return {string} Mapped text. diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index 072e20b4edde2..43e67c979b880 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -5,10 +5,10 @@ */ /** - * IIFE. + * Defines wp.teletype, which plays the scene. * * @param {Object} wp The global WordPress JS object. - * @param {Object} settings The settings object. + * @param {Object} settings Settings printed by wp_enqueue_command_palette_assets(). */ ( function ( wp, settings ) { 'use strict'; @@ -43,7 +43,7 @@ TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; /** - * Maps a string through the character tables. + * Maps a string from Dvorak to QWERTY key positions. * * @param {string} value Text to map. * @return {string} Mapped text. @@ -85,7 +85,7 @@ canvas.setAttribute( 'aria-hidden', 'true' ); /** - * Recalculate canvas size on resize event. + * Sizes the canvas to its parent and adds or drops columns to fit. */ function resize() { var ratio = window.devicePixelRatio || 1, @@ -105,11 +105,7 @@ count = Math.ceil( width / RAIN_SIZE ); - /* - * Columns keep their position across a resize so the rain does not restart, - * and any new ones start at a random height so the edge does not arrive as - * a straight line. - */ + // New columns start at a random height so they do not arrive as a straight line. for ( i = columns.length; i < count; i++ ) { columns.push( Math.random() * height ); } @@ -118,7 +114,7 @@ } /** - * Draw the rain. + * Draws one row of glyphs and advances each column. */ function draw() { var i; @@ -144,9 +140,9 @@ } /** - * Rain animation frame tick event. + * Draws a row once every RAIN_SPEED milliseconds. * - * @param {DOMHighResTimeStamp} now Event timestamp. + * @param {DOMHighResTimeStamp} now Current frame time. */ function tick( now ) { frame = window.requestAnimationFrame( tick ); @@ -191,10 +187,6 @@ var style = document.createElement( 'style' ); style.textContent = STYLE; - /* - * The scene is a modal dialog so that it is reachable and escapable rather than - * something that happens silently over the top of an admin page. - */ var overlay = document.createElement( 'div' ); overlay.className = 'wp-teletype'; overlay.setAttribute( 'role', 'dialog' ); @@ -202,14 +194,12 @@ overlay.setAttribute( 'aria-label', i18n.label || 'Press Escape to leave.' ); overlay.setAttribute( 'tabindex', '-1' ); - // Typed a character at a time, so it reaches the accessibility tree as whole - // lines through the live region below instead. + // Hidden from screen readers, which get whole lines from the live region. var line = document.createElement( 'p' ); line.setAttribute( 'aria-hidden', 'true' ); line.setAttribute( 'lang', 'en' ); - // The dialogue is English whatever the profile language, so it says so. The - // dialog label and Exit button are translated and inherit the page language. + // The dialogue is untranslated; the label and Exit button inherit the page language. var narration = document.createElement( 'div' ); narration.className = 'narration'; narration.setAttribute( 'lang', 'en' ); @@ -240,10 +230,10 @@ } /** - * setTimout helper. + * Calls a function after a delay unless the scene has ended. * * @param {number} ms Milliseconds to wait. - * @param {callback} next Callback function. + * @param {Function} next Function to call. */ function wait( ms, next ) { timer = window.setTimeout( function () { @@ -254,7 +244,7 @@ } /** - * Clear the first line. + * Removes the typed text, leaving the cursor. */ function clear() { while ( line.firstChild !== cursor ) { @@ -263,7 +253,7 @@ } /** - * Add a new line. + * Starts a new line ahead of the cursor. */ function newline() { line.insertBefore( document.createElement( 'br' ), cursor ); @@ -272,8 +262,8 @@ /** * Types one line, a character at a time, ahead of the cursor. * - * @param {string} text String to type. - * @param {callback} done Event callback at completion. + * @param {string} text Text to type. + * @param {Function} done Called once the line is typed. */ function type( text, done ) { var chars = text.split( '' ); @@ -294,9 +284,9 @@ } /** - * Act one: typed on the lights-on screen, each line under the last. + * Types act one on the white screen, each line under the last. * - * @param {number} index Line index to type. + * @param {number} index Index of the line to type. */ function actOne( index ) { if ( index >= ACT_ONE.length ) { @@ -314,7 +304,7 @@ } /** - * The turn: screen to black, text to green, rain starts, cursor fades up alone. + * Switches the screen to black, starts the rain, and fades the cursor in. */ function lightsOut() { overlay.className = 'wp-teletype is-dark'; @@ -335,10 +325,9 @@ } /** - * Act two: one line at a time, cleared between each. The last line holds, then - * the scene takes itself down and gives the admin page back. + * Types act two one line at a time, then ends the scene after the last line. * - * @param {number} index Line index to type. + * @param {number} index Index of the line to type. */ function actTwo( index ) { var text = dvortr( ACT_TWO[ index ] ).replace( '%s', function () { @@ -360,7 +349,7 @@ } /** - * End the easter egg. + * Ends the scene and restores the page, its scrolling, and focus. */ function abort() { if ( aborted ) { @@ -385,9 +374,9 @@ } /** - * Keydown event listener while easter egg running. + * Exits on Escape and keeps Tab on the Exit button. * - * @param {KeyboardEvent} event Fired keyboard event. + * @param {KeyboardEvent} event The keydown event. */ function onKeydown( event ) { if ( 'Escape' === event.key ) { From c743895e1d4a6d3a859b4a14d4b03856c761ec81 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Tue, 20 Oct 2026 16:48:36 -0700 Subject: [PATCH 24/25] Administration: Tidy the teletype styles. The heredoc had stray blank lines inside rules and a misindented `@keyframes` block that made the `@media` query look nested inside it. The comment above it now says only what the styles are and who injects them. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Ww1dPJfU7CxchvCQiycHbo --- src/wp-includes/script-loader.php | 33 +++++++++++-------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 92ce36cb7f1f8..443064718f19e 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -3960,13 +3960,7 @@ function wp_enqueue_command_palette_assets() { ) ); - /* - * Styles for teletype easter egg. - * - * These are added to the DOM via the teletype JavaScript if the - * easter egg is activated. Stored here to allow for HEREDOC - * highlighting by IDEs. - */ + // Styles for the teletype easter egg, injected by teletype.js when it runs. $teletype_styles = <<<'CSS' .wp-teletype { position:fixed; @@ -4005,25 +3999,22 @@ function wp_enqueue_command_palette_assets() { width:1px; height:1px; margin:-1px; - padding:0; overflow:hidden; clip-path:inset(50%); white-space:nowrap; border:0 } - .wp-teletype .exit{ + .wp-teletype .exit { position:absolute; z-index:2; top:1.5em; right:1.5em; - padding:.7em 1.8em; border:0; border-radius:999px; background:#2271b1; color:#fff; - font-family:inherit; font-size:14px; line-height:1; @@ -4036,26 +4027,26 @@ function wp_enqueue_command_palette_assets() { outline:2px solid #fff; outline-offset:2px } - .wp-teletype .cursor{ + .wp-teletype .cursor { opacity:0; transition:opacity linear; - transition-duration: var( --wp-teletype-transition-duration ); /* Set in JS file */ + transition-duration:var(--wp-teletype-transition-duration) /* Set in teletype.js. */ } - - .wp-teletype .cursor.is-visible{ + .wp-teletype .cursor.is-visible { opacity:1; animation:wp-teletype-blink 1s step-end infinite } - - @keyframes wp-teletype-blink{ + @keyframes wp-teletype-blink { 50% { - opacity:0} + opacity:0 } - @media (prefers-reduced-motion:reduce) { - .wp-teletype .cursor.is-visible{ + } + @media (prefers-reduced-motion:reduce) { + .wp-teletype .cursor.is-visible { animation:none } - .wp-teletype,.wp-teletype .cursor { + .wp-teletype, + .wp-teletype .cursor { transition:none } } From 303769b4428d3c0673e467693bba23add4476a39 Mon Sep 17 00:00:00 2001 From: adamsilverstein Date: Wed, 21 Oct 2026 14:57:09 -0700 Subject: [PATCH 25/25] Administration: Make the teletype strings translatable. The trigger phrase and the scene's lines were stored through a keyboard cipher and only ever played in English. Pass them from PHP through `__()` instead, with translator comments pointing at #65907, so locales can translate the dialogue and pick a trigger phrase of their own. The trigger is compared with case, whitespace and punctuation stripped, which now covers CJK punctuation too, and the `lang="en"` markers are dropped now that the dialogue follows the profile language. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Jr7idXoLVhrnR5SnnVRJe5 --- src/js/_enqueues/admin/teletype-loader.js | 31 ++++------ src/js/_enqueues/admin/teletype.js | 72 ++++++----------------- src/wp-includes/script-loader.php | 30 +++++++++- 3 files changed, 55 insertions(+), 78 deletions(-) diff --git a/src/js/_enqueues/admin/teletype-loader.js b/src/js/_enqueues/admin/teletype-loader.js index e001b850ddbac..f35be84c719bc 100644 --- a/src/js/_enqueues/admin/teletype-loader.js +++ b/src/js/_enqueues/admin/teletype-loader.js @@ -13,33 +13,22 @@ ( function ( wp, settings ) { 'use strict'; - if ( ! wp || ! wp.data || ! settings ) { + if ( ! wp || ! wp.data || ! settings || ! settings.i18n ) { return; } - var KEY = 'ishdg;rsdkot', - FROM = '\',.pyfgcrl/=\\aoeuidhtns-;qjkxbmwvz"<>PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ[]', - TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; - /** - * Maps a string from Dvorak to QWERTY key positions. + * Lowercases text and strips its whitespace and punctuation. * - * @param {string} value Text to map. - * @return {string} Mapped text. + * @param {string} value Text to normalize. + * @return {string} Normalized text. */ - function dvortr( value ) { - var map = {}, - i; - - for ( i = 0; i < FROM.length; i++ ) { - map[ FROM.charAt( i ) ] = TO.charAt( i ); - } - - return value.replace( /[\s\S]/g, function ( character ) { - return map[ character ] || character; - } ); + function normalize( value ) { + return value.toLowerCase().replace( /[\s!-\/:-@\[-`{-~\u3000-\u303f\uff01-\uff0f]/g, '' ); } + var trigger = normalize( settings.i18n.trigger || '' ); + /** * Loads the teletype if needed, then runs it. */ @@ -91,9 +80,9 @@ return; } - value = input.value.toLowerCase().replace( /[^a-z0-9]/g, '' ); + value = normalize( input.value ); - if ( ! value || dvortr( value ) !== KEY ) { + if ( ! value || value !== trigger ) { return; } diff --git a/src/js/_enqueues/admin/teletype.js b/src/js/_enqueues/admin/teletype.js index 43e67c979b880..4286797df5e4f 100644 --- a/src/js/_enqueues/admin/teletype.js +++ b/src/js/_enqueues/admin/teletype.js @@ -24,43 +24,6 @@ // Half-width katakana and digits. var GLYPHS = 'アイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワヲン0123456789'; - var ACT_ONE = [ - 'O.nu[p.u.p.bj. e.y.jy.ev', - 'Cbcycaycbi cbucbcy. nrrl .ojd.,an lpryrjrnv', - 'Pgbbcbi a prgycb. macby.babj. jfjn.v Xajt cbvvv 3', - '2', - '1' - ]; - - // %s is replaced with the current user's display name. - var ACT_TWO = [ - 'PYFGCRL?+|AOEUIDHTNS_:QJKXBMWVZ[]', - TO = 'qwertyuiop[]\\asdfghjkl;\'zxcvbnm,./QWERTYUIOP{}|ASDFGHJKL:"ZXCVBNM<>?-='; - - /** - * Maps a string from Dvorak to QWERTY key positions. - * - * @param {string} value Text to map. - * @return {string} Mapped text. - */ - function dvortr( value ) { - var map = {}, - i; - - for ( i = 0; i < FROM.length; i++ ) { - map[ FROM.charAt( i ) ] = TO.charAt( i ); - } - - return value.replace( /[\s\S]/g, function ( character ) { - return map[ character ] || character; - } ); - } - var STYLE = [ '.wp-teletype { --wp-teletype-transition-duration: ' + FADE_TIME / 1000 + 's }', settings.styles, @@ -170,19 +133,21 @@ /** * Plays the scene. * - * @param {string} displayName The current user's display name. - * @param {Object} [i18n] Translated interface strings. - * @param {string} [i18n.label] Accessible name of the dialog. - * @param {string} [i18n.exit] Text of the Exit button. + * @param {string} displayName The current user's display name. + * @param {Object} i18n Translated strings. + * @param {string} i18n.label Accessible name of the dialog. + * @param {string} i18n.exit Text of the Exit button. + * @param {string} i18n.rain Description of the rain for screen readers. + * @param {string[]} i18n.actOne Lines typed on the white screen. + * @param {string[]} i18n.actTwo Lines typed on the black screen; %s is the display name. */ function run( displayName, i18n ) { var aborted = false, timer = null, stopRain = null, previousFocus = document.activeElement, - previousOverflow = document.documentElement.style.overflow; - - i18n = i18n || {}; + previousOverflow = document.documentElement.style.overflow, + actOneLines = i18n.actOne.concat( [ '2', '1' ] ); var style = document.createElement( 'style' ); style.textContent = STYLE; @@ -191,25 +156,22 @@ overlay.className = 'wp-teletype'; overlay.setAttribute( 'role', 'dialog' ); overlay.setAttribute( 'aria-modal', 'true' ); - overlay.setAttribute( 'aria-label', i18n.label || 'Press Escape to leave.' ); + overlay.setAttribute( 'aria-label', i18n.label ); overlay.setAttribute( 'tabindex', '-1' ); // Hidden from screen readers, which get whole lines from the live region. var line = document.createElement( 'p' ); line.setAttribute( 'aria-hidden', 'true' ); - line.setAttribute( 'lang', 'en' ); - // The dialogue is untranslated; the label and Exit button inherit the page language. var narration = document.createElement( 'div' ); narration.className = 'narration'; - narration.setAttribute( 'lang', 'en' ); narration.setAttribute( 'aria-live', 'polite' ); narration.setAttribute( 'aria-atomic', 'true' ); var exit = document.createElement( 'button' ); exit.type = 'button'; exit.className = 'exit'; - exit.textContent = i18n.exit || 'Exit'; + exit.textContent = i18n.exit; var cursor = document.createElement( 'span' ); cursor.className = 'cursor'; @@ -289,13 +251,13 @@ * @param {number} index Index of the line to type. */ function actOne( index ) { - if ( index >= ACT_ONE.length ) { + if ( index >= actOneLines.length ) { lightsOut(); return; } - say( dvortr( ACT_ONE[ index ] ) ); - type( dvortr( ACT_ONE[ index ] ), function () { + say( actOneLines[ index ] ); + type( actOneLines[ index ], function () { newline(); wait( LINE_PAUSE, function () { actOne( index + 1 ); @@ -317,7 +279,7 @@ } // Describe the rain once for screen readers. - say( 'The screen goes black. Green code rains down it.' ); + say( i18n.rain ); wait( ACT_PAUSE, function () { actTwo( 0 ); @@ -330,13 +292,13 @@ * @param {number} index Index of the line to type. */ function actTwo( index ) { - var text = dvortr( ACT_TWO[ index ] ).replace( '%s', function () { + var text = i18n.actTwo[ index ].replace( '%s', function () { return displayName; } ); say( text ); type( text, function () { - if ( index + 1 >= ACT_TWO.length ) { + if ( index + 1 >= i18n.actTwo.length ) { wait( ACT_PAUSE, abort ); return; } diff --git a/src/wp-includes/script-loader.php b/src/wp-includes/script-loader.php index 443064718f19e..bf960b4728b50 100644 --- a/src/wp-includes/script-loader.php +++ b/src/wp-includes/script-loader.php @@ -4067,9 +4067,35 @@ function wp_enqueue_command_palette_assets() { ), 'name' => wp_get_current_user()->display_name, 'styles' => str_replace( array( "\n", "\t" ), '', $teletype_styles ), + /* + * The teletype is an easter egg: typing the trigger phrase into the + * command palette plays a short scene. See #65907 for the details. + */ 'i18n' => array( - 'label' => __( 'Press Escape to leave.' ), - 'exit' => __( 'Exit' ), + /* translators: Easter egg trigger, typed into the command palette. Case, spaces and punctuation are ignored. Feel free to pick a phrase loved by your local community. See https://core.trac.wordpress.org/ticket/65907 */ + 'trigger' => __( 'Code is poetry' ), + /* translators: Accessible name of the easter egg dialog. See https://core.trac.wordpress.org/ticket/65907 */ + 'label' => __( 'A WordPress easter egg. Press Escape to leave.' ), + /* translators: Button that closes the easter egg. See https://core.trac.wordpress.org/ticket/65907 */ + 'exit' => __( 'Exit' ), + /* translators: Read aloud to screen readers when the easter egg screen goes black and Matrix-style code rain starts. See https://core.trac.wordpress.org/ticket/65907 */ + 'rain' => __( 'The screen goes black. Green code rains down it.' ), + 'actOne' => array( + /* translators: Easter egg, typed out like a computer terminal. See https://core.trac.wordpress.org/ticket/65907 */ + __( 'Self-reference detected.' ), + /* translators: Easter egg, typed out like a computer terminal. See https://core.trac.wordpress.org/ticket/65907 */ + __( 'Initiating infinite loop eschewal protocol.' ), + /* translators: Easter egg, typed out like a computer terminal. The 3 starts a countdown that continues 2, 1. See https://core.trac.wordpress.org/ticket/65907 */ + __( 'Running a routine maintenance cycle. Back in... 3' ), + ), + 'actTwo' => array( + /* translators: %s: User display name. Easter egg quoting The Matrix (1999); use the line from your local release of the film if there is one. See https://core.trac.wordpress.org/ticket/65907 */ + __( 'Wake up, %s...' ), + /* translators: Easter egg quoting The Matrix (1999); use the line from your local release of the film if there is one. See https://core.trac.wordpress.org/ticket/65907 */ + __( 'The Matrix has you...' ), + /* translators: Easter egg quoting The Matrix (1999); use the line from your local release of the film if there is one. See https://core.trac.wordpress.org/ticket/65907 */ + __( 'Follow the white rabbit.' ), + ), ), ), JSON_HEX_TAG | JSON_UNESCAPED_SLASHES