Making WordPress.org

Changeset 14950


Ignore:
Timestamp:
06/08/2026 10:45:07 AM (2 months ago)
Author:
thilinah
Message:

Plugin Directory: server-render screenshots gallery + Gallery Lightbox Enhancements polyfill

Props alexodiy, thilinah, dd32, bor0.
Closes https://github.com/WordPress/wordpress.org/pull/622.
Fixes #2273, #8083, #8204, #5190, #7878.

Location:
sites/trunk/wordpress.org/public_html/wp-content
Files:
14 added
5 deleted
4 edited

Legend:

Unmodified
Added
Removed
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/class-plugin-directory.php

    r14932 r14950  
    469469                add_shortcode( Shortcodes\Release_Confirmation::SHORTCODE, array( __NAMESPACE__ . '\Shortcodes\Release_Confirmation', 'display' ) );
    470470                add_action( 'template_redirect', array( __NAMESPACE__ . '\Shortcodes\Release_Confirmation', 'template_redirect' ) );
     471
     472                add_filter( 'wp_resource_hints', array( __NAMESPACE__ . '\Shortcodes\Screenshots', 'add_resource_hints' ), 10, 2 );
    471473        }
    472474
  • sites/trunk/wordpress.org/public_html/wp-content/plugins/plugin-directory/shortcodes/class-screenshots.php

    r8729 r14950  
    11<?php
     2/**
     3 * The [wporg-plugins-screenshots] shortcode.
     4 *
     5 * @package WordPressdotorg\Plugin_Directory\Shortcodes
     6 */
     7
    28namespace WordPressdotorg\Plugin_Directory\Shortcodes;
    39
    410use WordPressdotorg\Plugin_Directory\Template;
    5 use WordPressdotorg\Plugin_Directory\Plugin_i18n;
    611
    712/**
    8  * The [wporg-plugins-screenshots] shortcode handler to display a plugins screenshots.
     13 * The [wporg-plugins-screenshots] shortcode handler.
     14 *
     15 * Renders the screenshot gallery server-side via core/gallery + core/image
     16 * blocks with the lightbox enabled on each image. The gallery uses CSS
     17 * multi-column flow ("brick" / masonry layout) so every screenshot keeps
     18 * its natural aspect ratio — important because plugin authors upload
     19 * shots in widely different proportions (portrait phone, landscape
     20 * desktop, panoramas) and a fixed-aspect grid would crop them.
     21 *
     22 * For galleries with more than `REVEAL_THRESHOLD` screenshots, every
     23 * figure still renders into the DOM upfront and CSS clips the wrap at
     24 * a fixed `max-height: 32rem`. A single "Show all N screenshots"
     25 * button below the grid releases the clip on click and is removed
     26 * afterwards (one-time reveal). On mobile the collapse logic is
     27 * suppressed entirely — vertical scroll is natural, so every
     28 * screenshot renders straight away.
     29 *
     30 * Lightbox captions and the optional masonry style variation for any
     31 * other Gallery block ship through the Gallery Lightbox Enhancements
     32 * plugin (force-loaded via `mu-plugins/`), polyfilling pending core
     33 * pull requests until they land in Gutenberg.
    934 *
    1035 * @package WordPressdotorg\Plugin_Directory\Shortcodes
     
    1338
    1439        /**
     40         * At this count or below we render every screenshot upfront — no button.
     41         *
     42         * Per dd32's brief: "optimize for fewer than 10 screenshots (95% of
     43         * plugins)". Inside that 95% the simplest possible UI is best — the
     44         * grid alone, no extra moving parts.
     45         *
     46         * @var int
     47         */
     48        const REVEAL_THRESHOLD = 9;
     49
     50        /**
     51         * Pseudo attachment-id offset for screenshots.
     52         *
     53         * Plugin screenshots are external assets on `ps.w.org` and have no real
     54         * attachment ID. The core Image block's lightbox needs a stable numeric
     55         * key in `state.metadata.{id}`, so we mint one from this offset plus the
     56         * screenshot index.
     57         *
     58         * @var int
     59         */
     60        const SYNTHETIC_ID_OFFSET = 9000000;
     61
     62        /**
     63         * Renders the shortcode output.
     64         *
    1565         * @return string
    1666         */
    17         static function display() {
    18                 $output = '';
    19 
     67        public static function display() {
    2068                $screenshots = Template::get_screenshots();
    2169
    22                 if ( ! $screenshots ) {
     70                if ( empty( $screenshots ) ) {
    2371                        return '';
    2472                }
    2573
    26                 foreach ( $screenshots as $image ) {
    27                         $screen_shot = sprintf(
    28                                 '<a href="%1$s" rel="nofollow"><img class="screenshot" src="%1$s" alt="" /></a>',
    29                                 esc_url( $image['src'] )
     74                self::enqueue_assets();
     75
     76                $count      = count( $screenshots );
     77                $use_reveal = ( $count > self::REVEAL_THRESHOLD );
     78
     79                // Pluck each screenshot's intrinsic width / height from the
     80                // `assets_screenshots` post meta — populated once during plugin
     81                // import (see `Import::enrich_asset_dimensions()`, r14866) and
     82                // already inlined on every entry of $screenshots by
     83                // `Template::get_screenshots()`. Shipping the dimensions on the
     84                // `<img>` tag lets the browser reserve slot height from the
     85                // aspect ratio at parse time, so the gallery has zero layout
     86                // shift no matter how slowly the screenshots decode.
     87                $dimensions = self::get_dimensions( $screenshots );
     88
     89                // Always render every figure — masonry balances across columns
     90                // once on first paint, never again. Collapse is purely a CSS
     91                // `max-height` clip on the wrap, so the click doesn't re-flow
     92                // figures (which is what made the previous "display:none on
     93                // hidden tiles" implementation read as "screenshots reshuffle"
     94                // after Show all).
     95                //
     96                // Strip the auto-injected layout classes from our gallery only.
     97                // Gutenberg's flex layout system adds `is-layout-flex` /
     98                // `wp-block-gallery-is-layout-flex` and a per-block container
     99                // class; they fight our row-aligned grid and would otherwise
     100                // force every layout rule to use `!important`. The filter
     101                // scopes the change to galleries that carry our marker class,
     102                // so other Gallery blocks on the page render unchanged.
     103                add_filter( 'render_block_core/gallery', array( __CLASS__, 'strip_layout_classes' ), 20, 1 );
     104                $markup   = self::build_gallery_markup( $screenshots, $count, $dimensions );
     105                $rendered = do_blocks( $markup );
     106                remove_filter( 'render_block_core/gallery', array( __CLASS__, 'strip_layout_classes' ), 20 );
     107
     108                if ( $use_reveal ) {
     109                        $rendered = self::wrap_with_show_all_button( $rendered, $count );
     110                }
     111
     112                return $rendered;
     113        }
     114
     115        /**
     116         * Strips Gutenberg's flex layout helper classes from our gallery
     117         * wrapper before it reaches the browser.
     118         *
     119         * The block layout system injects `is-layout-flex` and the
     120         * per-block `wp-block-gallery-is-layout-flex` / `wp-block-gallery-N`
     121         * classes; together they pull a flex container with row/wrap
     122         * defaults that fights every CSS rule we write for the screenshot
     123         * grid. Removing them at render time lets `screenshots.css` use
     124         * plain (non-`!important`) selectors and keeps the change scoped
     125         * to galleries that carry our `is-style-screenshots` marker — any
     126         * other Gallery block on the page renders unchanged.
     127         *
     128         * @param string $content Rendered Gallery block markup.
     129         * @return string
     130         */
     131        public static function strip_layout_classes( $content ) {
     132                if ( ! is_string( $content ) || false === strpos( $content, 'is-style-screenshots' ) ) {
     133                        return $content;
     134                }
     135
     136                $processor = new \WP_HTML_Tag_Processor( $content );
     137                while ( $processor->next_tag( 'figure' ) ) {
     138                        if ( ! $processor->has_class( 'is-style-screenshots' ) ) {
     139                                continue;
     140                        }
     141
     142                        $processor->remove_class( 'is-layout-flex' );
     143                        $processor->remove_class( 'wp-block-gallery-is-layout-flex' );
     144
     145                        // Also drop `wp-block-gallery-N` — auto-generated by the
     146                        // layout system, useful only for the styles we're disabling.
     147                        $class_attr = $processor->get_attribute( 'class' );
     148                        if ( is_string( $class_attr ) ) {
     149                                $cleaned = preg_replace( '/\s*\bwp-block-gallery-\d+\b\s*/', ' ', $class_attr );
     150                                $cleaned = trim( preg_replace( '/\s+/', ' ', $cleaned ) );
     151                                if ( $cleaned !== $class_attr ) {
     152                                        $processor->set_attribute( 'class', $cleaned );
     153                                }
     154                        }
     155                        break;
     156                }
     157
     158                return $processor->get_updated_html();
     159        }
     160
     161        /**
     162         * Adds preconnect / dns-prefetch hints to the Photon CDN host on
     163         * single-plugin pages so the browser can warm up the TLS handshake
     164         * while the page HTML is still streaming. Saves ~50–150 ms on the
     165         * first thumbnail paint for cold visitors. Hooked from
     166         * `class-plugin-directory.php` via the `wp_resource_hints` filter.
     167         *
     168         * @param array  $urls          Resource hint URLs already queued for $relation_type.
     169         * @param string $relation_type One of preconnect / dns-prefetch / prerender / prefetch.
     170         * @return array
     171         */
     172        public static function add_resource_hints( $urls, $relation_type ) {
     173                if ( ! is_singular( 'plugin' ) ) {
     174                        return $urls;
     175                }
     176
     177                if ( 'preconnect' === $relation_type ) {
     178                        $urls[] = array(
     179                                'href'        => 'https://i0.wp.com',
     180                                'crossorigin' => 'anonymous',
    30181                        );
    31 
    32                         if ( $image['caption'] ) {
    33                                 $screen_shot .= '<figcaption>' . $image['caption'] . '</figcaption>';
     182                } elseif ( 'dns-prefetch' === $relation_type ) {
     183                        $urls[] = 'https://i0.wp.com';
     184                }
     185
     186                return $urls;
     187        }
     188
     189        /**
     190         * Enqueues the shortcode's own CSS and toggle script. Assets live
     191         * under the plugin's `css/` and `js/` directories alongside the
     192         * other Plugin Directory stylesheets and scripts.
     193         */
     194        protected static function enqueue_assets() {
     195                wp_enqueue_style(
     196                        'wporg-plugins-screenshots',
     197                        plugins_url( 'css/screenshots.css', __DIR__ . '/../plugin-directory.php' ),
     198                        array(),
     199                        (string) filemtime( __DIR__ . '/../css/screenshots.css' )
     200                );
     201
     202                wp_enqueue_script(
     203                        'wporg-plugins-screenshots',
     204                        plugins_url( 'js/screenshots.js', __DIR__ . '/../plugin-directory.php' ),
     205                        array(),
     206                        (string) filemtime( __DIR__ . '/../js/screenshots.js' ),
     207                        true
     208                );
     209        }
     210
     211        /**
     212         * Builds the block markup string for the Gallery + Image blocks.
     213         *
     214         * Every screenshot renders into the markup — collapse / expand is
     215         * a CSS `max-height` clip on the gallery wrap, not a DOM hide. This
     216         * keeps the masonry balanced across columns once and prevents the
     217         * "figures reshuffle" effect the user saw on click previously.
     218         *
     219         * @param array $screenshots Screenshots returned by Template::get_screenshots().
     220         * @param int   $count       Total screenshot count, used to size columns.
     221         * @param array $dimensions  Optional `[ url => [ width, height ] ]` map
     222         *                           from {@see self::get_dimensions()}; consumed by
     223         *                           build_image_block() to emit intrinsic width /
     224         *                           height attrs and prevent layout shift.
     225         * @return string Block markup ready for do_blocks().
     226         */
     227        protected static function build_gallery_markup( $screenshots, $count, $dimensions = array() ) {
     228                $inner = '';
     229                $index = 0;
     230
     231                foreach ( $screenshots as $screenshot_num => $screenshot ) {
     232                        // Every figure loads eagerly — this is a detail page, not
     233                        // a list, so users land here already committed to seeing
     234                        // the screenshots. Lazy-loading would force a layout-shift
     235                        // growth animation after the user clicks "Show all" and
     236                        // would also hide the screenshots from third-party
     237                        // scrapers / SEO crawlers that read the rendered HTML.
     238                        // Tiles past the visible cap drop to `fetchpriority="low"`
     239                        // so they don't fight the LCP image for bandwidth, but
     240                        // the request still fires immediately.
     241                        $is_above_fold = ( $index < self::REVEAL_THRESHOLD );
     242                        $shot_url      = isset( $screenshot['src'] ) ? (string) $screenshot['src'] : '';
     243                        $shot_dims     = ( $shot_url && isset( $dimensions[ $shot_url ] ) ) ? $dimensions[ $shot_url ] : null;
     244                        $inner        .= self::build_image_block(
     245                                $screenshot,
     246                                self::SYNTHETIC_ID_OFFSET + (int) $screenshot_num,
     247                                $is_above_fold,
     248                                $shot_dims
     249                        );
     250
     251                        ++$index;
     252                }
     253
     254                /*
     255                 * Layout class drives CSS rules on the gallery wrapper.
     256                 *
     257                 * `is-count-N` (1-3): tiny galleries fit a single row at full
     258                 * size, no anchor logic. `has-tail-1` (count % 3 == 1): when
     259                 * `has-uniform-aspect` is also present, the last figure spans
     260                 * the full row instead of orphaning in the rightmost column.
     261                 * Without `has-uniform-aspect` the anchor class is a no-op, so
     262                 * the CSS-columns brick layout is unaffected.
     263                 *
     264                 * `has-uniform-aspect` is set by self::is_uniform_aspect() —
     265                 * always for N=2-4, dimensions-based for N>=5 (read straight
     266                 * from each screenshot's `assets_screenshots` record). Default
     267                 * layout is the CSS-columns brick masonry that keeps every
     268                 * screenshot's natural aspect ratio;
     269                 * the uniform-aspect upgrade swaps to a row-aligned CSS grid
     270                 * when figures share a shape so rows do not orphan tiles. Output
     271                 * stays identical across viewports — fully cacheable by Varnish
     272                 * and object cache.
     273                 */
     274                $layout_class = '';
     275                if ( $count >= 1 && $count <= 3 ) {
     276                        // Tiny galleries — `is-count-N` rules in CSS pin the column
     277                        // count. N=1 = single tile; N=2 = row of 2; N=3 mixed flows
     278                        // across three columns, uniform-aspect renders as 2 + 1
     279                        // full-width.
     280                        $layout_class = ' is-count-' . (int) $count;
     281                } elseif ( 1 === ( $count % 3 ) ) {
     282                        // N=4, 7, 10, 13… — last figure anchors as full-width on
     283                        // uniform-aspect galleries; the class is a no-op when the
     284                        // CSS-columns brick remains the active layout (mixed
     285                        // aspect ratios).
     286                        $layout_class = ' has-tail-1';
     287                }
     288
     289                if ( self::is_uniform_aspect( $screenshots ) ) {
     290                        $layout_class .= ' has-uniform-aspect';
     291                }
     292
     293                $gallery_attrs = wp_json_encode(
     294                        array(
     295                                'linkTo'    => 'none',
     296                                'className' => trim( 'is-style-screenshots' . $layout_class ),
     297                        )
     298                );
     299
     300                $markup  = "<!-- wp:gallery {$gallery_attrs} -->\n";
     301                $markup .= '<figure class="wp-block-gallery has-nested-images columns-default is-style-screenshots' . esc_attr( $layout_class ) . '">' . "\n";
     302                $markup .= $inner;
     303                $markup .= "</figure>\n";
     304                $markup .= "<!-- /wp:gallery -->\n";
     305
     306                return $markup;
     307        }
     308
     309        /**
     310         * Builds a single core/image block with the lightbox enabled.
     311         *
     312         * @param array      $screenshot    Screenshot metadata.
     313         * @param int        $id            Synthetic attachment id used by the lightbox state.
     314         * @param bool       $above_fold    Whether the figure sits inside the visible window
     315         *                                  before the user clicks "Show all" (drives
     316         *                                  fetchpriority).
     317         * @param array|null $dimensions    `[ width, height ]` of the source PNG / JPEG / GIF,
     318         *                                  or null when the post meta did not carry both values.
     319         * @return string Image block markup.
     320         */
     321        protected static function build_image_block( $screenshot, $id, $above_fold = false, $dimensions = null ) {
     322                $src     = isset( $screenshot['src'] ) ? esc_url( $screenshot['src'] ) : '';
     323                $caption = isset( $screenshot['caption'] ) ? (string) $screenshot['caption'] : '';
     324
     325                if ( '' === $src ) {
     326                        return '';
     327                }
     328
     329                $alt = wp_strip_all_tags( $caption );
     330
     331                $attrs = wp_json_encode(
     332                        array(
     333                                'id'              => $id,
     334                                'sizeSlug'        => 'large',
     335                                'linkDestination' => 'none',
     336                                'lightbox'        => array( 'enabled' => true ),
     337                        )
     338                );
     339
     340                $srcset = self::photon_srcset( $src );
     341                $class  = 'wp-block-image size-large';
     342
     343                // `width` and `height` ship the screenshot's intrinsic
     344                // dimensions so the browser reserves layout space from the
     345                // aspect ratio at parse time. Without them every figure
     346                // collapses to a one-pixel slot until the image decodes — and
     347                // the gallery gallops down the page as figures resolve, an
     348                // awful CLS story for a high-traffic page like wordpress.org/
     349                // plugins/{slug}/.
     350                $dim_attrs = '';
     351                if ( is_array( $dimensions ) && isset( $dimensions[0], $dimensions[1] ) && $dimensions[0] > 0 && $dimensions[1] > 0 ) {
     352                        $dim_attrs = sprintf(
     353                                ' width="%d" height="%d"',
     354                                (int) $dimensions[0],
     355                                (int) $dimensions[1]
     356                        );
     357                }
     358
     359                // Every screenshot loads eagerly — this is a detail page where
     360                // the whole gallery is intentionally part of the document.
     361                // `fetchpriority` shapes the request order: the cap-window
     362                // thumbnails compete with the LCP image (high), the rest sit
     363                // behind them (low) so they download as bandwidth becomes
     364                // available without delaying the first paint.
     365                $priority = $above_fold ? 'high' : 'low';
     366
     367                // Wrap the `<img>` in an `<a href="{src}" target="_blank">` so
     368                // the no-JS reader still gets a path to the full-resolution
     369                // screenshot. The JS path cancels only this fallback navigation;
     370                // core's image lightbox click handlers still receive the event.
     371                $figure  = '<figure class="' . esc_attr( $class ) . '">';
     372                $figure .= sprintf(
     373                        '<a class="plugin-screenshots__fallback-link" href="%1$s" target="_blank" rel="noopener">',
     374                        $src
     375                );
     376                $figure .= sprintf(
     377                        '<img src="%1$s" alt="%2$s" class="wp-image-%3$d"%4$s%5$s loading="eager" fetchpriority="%6$s" decoding="async"/>',
     378                        $src,
     379                        esc_attr( $alt ),
     380                        $id,
     381                        $srcset,
     382                        $dim_attrs,
     383                        esc_attr( $priority )
     384                );
     385                $figure .= '</a>';
     386
     387                if ( '' !== $caption ) {
     388                        $figure .= sprintf(
     389                                '<figcaption class="wp-element-caption">%s</figcaption>',
     390                                wp_kses_post( $caption )
     391                        );
     392                }
     393
     394                $figure .= '</figure>';
     395
     396                return "<!-- wp:image {$attrs} -->\n{$figure}\n<!-- /wp:image -->\n";
     397        }
     398
     399        /**
     400         * Wraps the rendered gallery in a reveal container with a single
     401         * "Show all N screenshots" button below it. Click reveals every
     402         * hidden figure with a per-tile staggered fade-in and removes the
     403         * button (one-time reveal — no collapse-back affordance).
     404         *
     405         * The button sits below the gallery, matching the convention used
     406         * by other Plugin Directory pages with long lists (see "Show more"
     407         * on Description / Reviews sections).
     408         *
     409         * @param string $rendered_gallery Output of do_blocks() for the gallery.
     410         * @param int    $count            Total number of screenshots.
     411         * @return string
     412         */
     413        protected static function wrap_with_show_all_button( $rendered_gallery, $count ) {
     414                $label = sprintf(
     415                        /* translators: %s: total number of screenshots in the gallery. */
     416                        _n( 'Show all %s screenshot', 'Show all %s screenshots', $count, 'wporg-plugins' ),
     417                        number_format_i18n( $count )
     418                );
     419
     420                $button = sprintf(
     421                        '<button type="button" class="plugin-screenshots__show-all" aria-expanded="false">'
     422                        . '<span class="plugin-screenshots__show-all-label">%1$s</span>'
     423                        . '<span class="plugin-screenshots__show-all-chevron" aria-hidden="true"></span>'
     424                        . '</button>',
     425                        esc_html( $label )
     426                );
     427
     428                // The wrap clips the gallery via `max-height` + `overflow:
     429                // hidden`. Fade veil and button live on the reveal root so
     430                // they aren't clipped by that overflow — they absolute-position
     431                // to the wrap's bottom edge from outside.
     432                return '<div class="plugin-screenshots__reveal">'
     433                        . '<div class="plugin-screenshots__gallery-wrap">'
     434                        . $rendered_gallery
     435                        . '</div>'
     436                        . '<div class="plugin-screenshots__fade" aria-hidden="true"></div>'
     437                        . $button
     438                        . '</div>';
     439        }
     440
     441        /**
     442         * Builds a Photon-powered `srcset` (and matching `sizes`) attribute string
     443         * for a `ps.w.org` screenshot URL. Returns an empty string when the source
     444         * URL is not on `ps.w.org`, so the original `src` is used unchanged.
     445         *
     446         * Plugin authors upload screenshots at full resolution but we render them
     447         * inside a 3-column grid, so the browser otherwise downloads the full
     448         * asset (often 300–800 KB) only to scale it down to a ~250 px tile.
     449         * Routing the URL through `i0.wp.com` (Photon) returns a re-encoded,
     450         * width-bound copy at ~10× smaller payload — see
     451         * https://developer.wordpress.com/docs/photon/ for the resize/optim
     452         * options. The lightbox `src` (the unprefixed `ps.w.org` URL) stays the
     453         * full-resolution original so users get the lossless image when they
     454         * enlarge a screenshot.
     455         *
     456         * @param string $src Original asset URL.
     457         * @return string Attribute fragment ready to interpolate into `<img>`,
     458         *                including the leading space, or empty string.
     459         */
     460        protected static function photon_srcset( $src ) {
     461                if ( ! preg_match( '#^https?://ps\.w\.org/#', $src ) ) {
     462                        return '';
     463                }
     464
     465                // Photon (i0.wp.com) is a production-only optimisation. In local
     466                // or staging environments the proxy may not be reachable, which
     467                // would leave the gallery silently empty until the cold cache
     468                // warmed up. Fall back to the unoptimised `ps.w.org` URL there.
     469                $env = function_exists( 'wp_get_environment_type' ) ? wp_get_environment_type() : 'production';
     470                if ( 'production' !== $env ) {
     471                        return '';
     472                }
     473
     474                $photon_base = preg_replace( '#^https?://#', 'https://i0.wp.com/', $src );
     475                $widths      = array( 300, 600, 900 );
     476                $srcset      = array();
     477
     478                foreach ( $widths as $width ) {
     479                        $srcset[] = add_query_arg( 'w', $width, $photon_base ) . ' ' . $width . 'w';
     480                }
     481
     482                return sprintf(
     483                        ' srcset="%1$s" sizes="(max-width: 599px) 50vw, 33vw"',
     484                        esc_attr( implode( ', ', $srcset ) )
     485                );
     486        }
     487
     488        /**
     489         * Whether the gallery should swap from brick masonry to row-aligned grid.
     490         *
     491         * The default layout is the CSS multi-column "brick" flow — every
     492         * screenshot keeps its natural aspect ratio and figures stack like
     493         * masonry, which is what plugin authors usually upload (a mix of
     494         * portrait, landscape, and square shots). Grid is the better choice
     495         * only when the figure heights match, otherwise rows leave awkward
     496         * vertical gaps.
     497         *
     498         * Two paths:
     499         *   - N=2..4: always return true. Tiny galleries do not have enough
     500         *     figures for the brick effect to read, so a row-aligned grid
     501         *     gives a more predictable result. Same N now renders the same
     502         *     way regardless of upload — `prantorpay` and `iconic-copy-text-blocks`
     503         *     are both 4-tile + 1 full-width anchor, no surprises.
     504         *   - N>=5: compare aspect ratios pulled from each screenshot's
     505         *     `assets_screenshots` record. Width / height land in that meta
     506         *     during plugin import (`Import::enrich_asset_dimensions()`,
     507         *     r14866) and propagate to every $screenshot entry via
     508         *     `Template::get_screenshots()`, so the lookup is in-process
     509         *     and no network call runs at render time. A missing pair on
     510         *     any single screenshot trips the fallback to brick masonry —
     511         *     the safer layout when shapes might disagree.
     512         *
     513         * @param array $screenshots Output of {@see Template::get_screenshots()}.
     514         * @return bool True when the row-aligned grid layout should activate.
     515         */
     516        private static function is_uniform_aspect( $screenshots ) {
     517                $count = count( $screenshots );
     518                if ( $count < 2 ) {
     519                        return false;
     520                }
     521                if ( $count <= 4 ) {
     522                        // Tiny galleries always render as a tidy grid; brick masonry
     523                        // needs more figures to look like masonry.
     524                        return true;
     525                }
     526
     527                $dimensions = self::get_dimensions( $screenshots );
     528                if ( count( $dimensions ) !== $count ) {
     529                        // Probe missed at least one figure — fall back to brick
     530                        // masonry. The CSS-columns flow handles mixed aspects
     531                        // gracefully without us guessing.
     532                        return false;
     533                }
     534
     535                $aspects = array();
     536                foreach ( $dimensions as $dim ) {
     537                        if ( ! is_array( $dim ) || empty( $dim[1] ) ) {
     538                                return false;
    34539                        }
    35 
    36                         $output .= '<li><figure>' . $screen_shot . '</figure></li>';
    37                 }
    38 
    39                 return '<ul class="plugin-screenshots">' . $output . '</ul>';
     540                        $aspects[] = (float) $dim[0] / (float) $dim[1];
     541                }
     542
     543                $min = min( $aspects );
     544                $max = max( $aspects );
     545
     546                return ( $min > 0 ) && ( ( $max - $min ) / $min ) < 0.05;
     547        }
     548
     549        /**
     550         * Returns `[ url => [ width, height ] ]` for every screenshot.
     551         *
     552         * Width and height come straight off each screenshot record — they
     553         * are written into the `assets_screenshots` post meta during plugin
     554         * import (`Import::enrich_asset_dimensions()`, r14866) and inlined
     555         * onto every entry returned by `Template::get_screenshots()`, so the
     556         * lookup is a plain array read with no I/O. Records that did not
     557         * carry both dimensions (e.g. the import retrieve failed and a
     558         * backfill pass has not yet run) drop out of the map; downstream
     559         * code already treats missing entries as "no dimensions" and
     560         * degrades to brick masonry without `<img>` width/height.
     561         *
     562         * @param array $screenshots Screenshots returned by Template::get_screenshots().
     563         * @return array Map of screenshot URL → `[ width, height ]`. Entries
     564         *               are absent for screenshots whose meta record is
     565         *               missing width or height.
     566         */
     567        private static function get_dimensions( $screenshots ) {
     568                $dimensions = array();
     569
     570                foreach ( $screenshots as $shot ) {
     571                        if ( empty( $shot['src'] ) || empty( $shot['width'] ) || empty( $shot['height'] ) ) {
     572                                continue;
     573                        }
     574                        $dimensions[ (string) $shot['src'] ] = array( (int) $shot['width'], (int) $shot['height'] );
     575                }
     576
     577                return $dimensions;
    40578        }
    41579}
  • sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins-2024/client/styles/components/_components.scss

    r14140 r14950  
    1717@import "../../components/plugin/sections-developers";
    1818@import "../../components/plugin/sections-faq";
    19 @import "../../components/plugin/sections-image-gallery";
    2019@import "../../components/plugin/sections-reviews";
    21 @import "../../components/plugin/sections-screenshots";
    2220@import "../../components/plugin/sections-stats";
    2321@import "../../components/plugin/sections";
  • sites/trunk/wordpress.org/public_html/wp-content/themes/pub/wporg-plugins-2024/functions.php

    r14941 r14950  
    140140        }
    141141
    142         // React is currently only used on detail pages.
    143         if ( is_single() ) {
    144                 $assets_path = __DIR__ . '/build/theme.asset.php';
    145                 if ( file_exists( $assets_path ) ) {
    146                         $script_info = require( $assets_path );
    147                         wp_enqueue_script(
    148                                 'wporg-plugins-client',
    149                                 get_stylesheet_directory_uri() . '/build/theme.js',
    150                                 $script_info['dependencies'],
    151                                 $script_info['version'],
    152                                 true
    153                         );
    154                         wp_localize_script(
    155                                 'wporg-plugins-client',
    156                                 'localeData',
    157                                 array(
    158                                         '' => array(
    159                                                 'Plural-Forms' => _x( 'nplurals=2; plural=n != 1;', 'plural forms', 'wporg-plugins' ),
    160                                                 'Language'     => _x( 'en', 'language (fr, fr_CA)', 'wporg-plugins' ),
    161                                                 'localeSlug'   => _x( 'en', 'locale slug', 'wporg-plugins' ),
    162                                         ),
    163                                         'screenshots' => __( 'Screenshots', 'wporg-plugins' ),
    164                                 )
    165                         );
    166                 }
    167         }
    168 
    169142        // No Jetpack scripts needed.
    170143        add_filter( 'jetpack_implode_frontend_css', '__return_false' );
     
    197170                'wporg-plugins-locale-banner',
    198171                'wporg-plugins-stats',
    199                 'wporg-plugins-client',
    200172                'wporg-plugins-faq',
    201173        ];
Note: See TracChangeset for help on using the changeset viewer.