diff --git a/.github/workflows/reusable-performance-report-v2.yml b/.github/workflows/reusable-performance-report-v2.yml index 988e60310d950..bc0174d083aa0 100644 --- a/.github/workflows/reusable-performance-report-v2.yml +++ b/.github/workflows/reusable-performance-report-v2.yml @@ -104,7 +104,7 @@ jobs: env: BASE_SHA: ${{ steps.base-sha.outputs.result }} CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} - HOST_NAME: www.codevitals.run + HOST_NAME: codevitals.run run: | if [ -z "$CODEVITALS_PROJECT_TOKEN" ]; then echo "Performance results could not be published. 'CODEVITALS_PROJECT_TOKEN' is not set" diff --git a/.github/workflows/reusable-performance.yml b/.github/workflows/reusable-performance.yml index f211b58890fc3..37941678978ab 100644 --- a/.github/workflows/reusable-performance.yml +++ b/.github/workflows/reusable-performance.yml @@ -347,7 +347,7 @@ jobs: env: BASE_SHA: ${{ steps.base-sha.outputs.result }} CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} - HOST_NAME: "www.codevitals.run" + HOST_NAME: "codevitals.run" run: | if [ -z "$CODEVITALS_PROJECT_TOKEN" ]; then echo "Performance results could not be published. 'CODEVITALS_PROJECT_TOKEN' is not set" diff --git a/.gitignore b/.gitignore index 330a92ca02c7b..3997df4c9d603 100644 --- a/.gitignore +++ b/.gitignore @@ -38,9 +38,6 @@ wp-tests-config.php /src/wp-includes/blocks/* !/src/wp-includes/blocks/index.php /src/wp-includes/build -/src/wp-includes/class-wp-block-parser.php -/src/wp-includes/class-wp-block-parser-block.php -/src/wp-includes/class-wp-block-parser-frame.php /src/wp-includes/theme.json /packagehash.txt /.gutenberg-hash diff --git a/src/wp-admin/css/colors/_admin.scss b/src/wp-admin/css/colors/_admin.scss index 553037e3d30d1..97904e7e9e352 100644 --- a/src/wp-admin/css/colors/_admin.scss +++ b/src/wp-admin/css/colors/_admin.scss @@ -3,6 +3,7 @@ @forward 'variables' show $scheme-name, $base-color, $body-background, $button-color, $custom-welcome-panel, $dashboard-accent-1, $dashboard-accent-2, $dashboard-icon-background, $form-checked, $highlight-color, $icon-color, $link, $link-focus, $low-contrast-theme, $menu-bubble-text, $menu-collapse-focus-icon, $menu-collapse-text, $menu-highlight-background, $menu-highlight-icon, $menu-highlight-text, $menu-submenu-text, $menu-submenu-focus-text, $menu-submenu-background, $notification-color, $text-color; @use 'variables'; @use 'mixins'; +@use 'tokens'; /** * This function name uses British English to maintain backward compatibility, as developers @@ -37,13 +38,27 @@ span.wp-media-buttons-icon:before { color: currentColor; } -.wp-core-ui .button-link { - color: variables.$link; +/* Link button - appears as text link, no border or background */ +/* Matches Gutenberg's .is-link button variant */ +.wp-core-ui .button-link, +.wp-core-ui .button.button-link { + color: var(--wp-admin-theme-color); &:hover, - &:active, + &:active { + color: var(--wp-admin-theme-color-darker-20); + } + &:focus { - color: variables.$link-focus; + color: var(--wp-admin-theme-color); + border-radius: tokens.$radius-s; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 1px solid transparent; + } + + &:disabled, + &[aria-disabled="true"] { + color: tokens.$gray-600; } } @@ -51,7 +66,7 @@ span.wp-media-buttons-icon:before { .media-modal .trash-attachment, .media-modal .untrash-attachment, .wp-core-ui .button-link-delete { - color: #a00; + color: tokens.$alert-red; } .media-modal .delete-attachment:hover, @@ -62,7 +77,7 @@ span.wp-media-buttons-icon:before { .media-modal .untrash-attachment:focus, .wp-core-ui .button-link-delete:hover, .wp-core-ui .button-link-delete:focus { - color: #dc3232; + color: color.adjust(tokens.$alert-red, $lightness: 10%); } /* Forms */ @@ -109,79 +124,24 @@ textarea:focus { .wp-core-ui { + /* Default button - theme color border and text (matches secondary) */ .button { - border-color: #7e8993; - color: #32373c; - } - - .button.hover, - .button:hover, - .button.focus, - .button:focus { - border-color: color.adjust(#7e8993, $lightness: -5%); - color: color.adjust(#32373c, $lightness: -5%); - } - - .button.focus, - .button:focus { - border-color: #7e8993; - color: color.adjust(#32373c, $lightness: -5%); - box-shadow: 0 0 0 1px #32373c; - } - - .button:active { - border-color: #7e8993; - color: color.adjust(#32373c, $lightness: -5%); - box-shadow: none; - } - - .button.active, - .button.active:focus, - .button.active:hover { - border-color: variables.$button-color; - color: color.adjust(#32373c, $lightness: -5%); - box-shadow: inset 0 2px 5px -3px variables.$button-color; + @include mixins.button-secondary(); } - .button.active:focus { - box-shadow: 0 0 0 1px #32373c; - } - - @if ( variables.$low-contrast-theme != "true" ) { - .button, - .button-secondary { - color: variables.$highlight-color; - border-color: variables.$highlight-color; - } - - .button.hover, - .button:hover, - .button-secondary:hover{ - border-color: color.adjust(variables.$highlight-color, $lightness: -10%); - color: color.adjust(variables.$highlight-color, $lightness: -10%); - } - - .button.focus, - .button:focus, - .button-secondary:focus { - border-color: color.adjust(variables.$highlight-color, $lightness: 10%); - color: color.adjust(variables.$highlight-color, $lightness: -20%); - box-shadow: 0 0 0 1px color.adjust(variables.$highlight-color, $lightness: 10%); - } - - .button-primary { - &:hover { - color: #fff; - } - } + /* Secondary button - same as default */ + .button-secondary { + @include mixins.button-secondary(); } + /* Primary button - theme color background */ .button-primary { @include mixins.button( variables.$button-color ); } .button-group > .button.active { - border-color: variables.$button-color; + border-color: var(--wp-admin-theme-color); + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); } .wp-ui-primary { @@ -215,28 +175,34 @@ textarea:focus { /* List tables */ -@if variables.$low-contrast-theme == "true" { - .wrap .page-title-action:hover { - color: variables.$menu-text; - background-color: variables.$menu-background; - } -} @else { - .wrap .page-title-action, - .wrap .page-title-action:active { - border: 1px solid variables.$highlight-color; - color: variables.$highlight-color; - } - .wrap .page-title-action:hover { - color: color.adjust(variables.$highlight-color, $lightness: -10%); - border-color: color.adjust(variables.$highlight-color, $lightness: -10%); - } +// .page-title-action uses secondary button styling +.wrap .page-title-action { + background: transparent; + border: 1px solid var(--wp-admin-theme-color); + border-radius: tokens.$radius-s; + color: var(--wp-admin-theme-color); +} - .wrap .page-title-action:focus { - border-color: color.adjust(variables.$highlight-color, $lightness: 10%); - color: color.adjust(variables.$highlight-color, $lightness: -20%); - box-shadow: 0 0 0 1px color.adjust(variables.$highlight-color, $lightness: 10%); - } +.wrap .page-title-action:hover { + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); +} + +.wrap .page-title-action:focus { + background: transparent; + border-color: var(--wp-admin-theme-color); + color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 1px solid transparent; +} + +.wrap .page-title-action:active { + background: rgba(var(--wp-admin-theme-color--rgb), 0.08); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); + box-shadow: none; } .view-switch a.current:before { diff --git a/src/wp-admin/css/colors/_mixins.scss b/src/wp-admin/css/colors/_mixins.scss index d33cf3bb2d854..d783bf268613e 100644 --- a/src/wp-admin/css/colors/_mixins.scss +++ b/src/wp-admin/css/colors/_mixins.scss @@ -1,39 +1,132 @@ @use 'sass:color'; +@use 'tokens'; /* - * Button mixin- creates a button effect with correct - * highlights/shadows, based on a base color. + * Button mixin - creates a primary button effect. + * Uses CSS custom properties for theme color support across color schemes. */ @mixin button( $button-color, $button-text-color: #fff ) { - background: $button-color; - border-color: $button-color; + background: var(--wp-admin-theme-color); + border-color: transparent; + border-radius: tokens.$radius-s; color: $button-text-color; - &:hover, - &:focus { - background: color.adjust($button-color, $lightness: 3%); - border-color: color.adjust($button-color, $lightness: -3%); + &:hover { + background: var(--wp-admin-theme-color-darker-10); + border-color: transparent; color: $button-text-color; } &:focus { + background: var(--wp-admin-theme-color); + border-color: transparent; + color: $button-text-color; + /* Gutenberg-style focus ring: outer theme color + inset white for contrast */ box-shadow: - 0 0 0 1px #fff, - 0 0 0 3px $button-color; + 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color), + inset 0 0 0 1px tokens.$white; + /* Visible in Windows High Contrast mode */ + outline: 1px solid transparent; } &:active { - background: color.adjust($button-color, $lightness: -5%); - border-color: color.adjust($button-color, $lightness: -5%); + background: var(--wp-admin-theme-color-darker-20); + border-color: transparent; color: $button-text-color; } + &:disabled, + &.disabled { + background: tokens.$gray-100; + border-color: transparent; + color: tokens.$gray-600; + cursor: not-allowed; + } + &.active, &.active:focus, &.active:hover { - background: $button-color; + background: var(--wp-admin-theme-color-darker-10); color: $button-text-color; - border-color: color.adjust($button-color, $lightness: -15%); - box-shadow: inset 0 2px 5px -3px color.adjust($button-color, $lightness: -50%); + border-color: transparent; + box-shadow: none; + } +} + +/* + * Secondary button mixin - outlined style with theme color. + * Matches Gutenberg's .is-secondary button variant. + */ +@mixin button-secondary() { + background: transparent; + border: 1px solid var(--wp-admin-theme-color); + border-radius: tokens.$radius-s; + color: var(--wp-admin-theme-color); + + &:hover { + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); + } + + &:focus { + background: transparent; + border-color: var(--wp-admin-theme-color); + color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 1px solid transparent; + } + + &:active { + background: rgba(var(--wp-admin-theme-color--rgb), 0.08); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); + box-shadow: none; + } + + &:disabled, + &.disabled { + background: transparent; + border-color: tokens.$gray-300; + color: tokens.$gray-600; + cursor: not-allowed; + } +} + +/* + * Tertiary button mixin - transparent background, gray text. + */ +@mixin button-tertiary() { + background: transparent; + border: 1px solid tokens.$gray-600; + border-radius: tokens.$radius-s; + color: tokens.$gray-900; + + &:hover { + background: rgba(0, 0, 0, 0.05); + border-color: tokens.$gray-700; + color: tokens.$gray-900; + } + + &:focus { + background: transparent; + border-color: var(--wp-admin-theme-color); + color: tokens.$gray-900; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 1px solid transparent; + } + + &:active { + background: rgba(0, 0, 0, 0.1); + border-color: tokens.$gray-700; + color: tokens.$gray-900; + } + + &:disabled, + &.disabled { + background: transparent; + border-color: tokens.$gray-400; + color: tokens.$gray-600; + cursor: not-allowed; } } diff --git a/src/wp-admin/css/colors/_tokens.scss b/src/wp-admin/css/colors/_tokens.scss new file mode 100644 index 0000000000000..785f1f47f5076 --- /dev/null +++ b/src/wp-admin/css/colors/_tokens.scss @@ -0,0 +1,212 @@ +// ========================================================================== +// WordPress Design System Tokens +// ========================================================================== +// +// These tokens are derived from the WordPress Design System in Figma: +// https://www.figma.com/design/804HN2REV2iap2ytjRQ055/WordPress-Design-System +// +// IMPORTANT: Do NOT expose these as CSS custom properties. +// Use these Sass variables to compile to static CSS values. +// The only CSS custom properties available are those in wp-base-styles: +// - --wp-admin-theme-color +// - --wp-admin-theme-color--rgb +// - --wp-admin-theme-color-darker-10 +// - --wp-admin-theme-color-darker-20 +// - --wp-admin-border-width-focus +// +// ========================================================================== + + +// -------------------------------------------------------------------------- +// Grid Units (Spacing) +// -------------------------------------------------------------------------- +// Based on 4px base unit. Use for padding, margin, and gap values. + +$grid-unit-05: 4px; // Scales/grid unit 05 +$grid-unit-10: 8px; // Scales/grid unit 10 +$grid-unit-15: 12px; // Scales/grid unit 15 +$grid-unit-20: 16px; // Scales/grid unit 20 +$grid-unit-30: 24px; // Scales/grid unit 30 +$grid-unit-40: 32px; // Scales/grid unit 40 +$grid-unit-50: 40px; // Scales/grid unit 50 +$grid-unit-60: 48px; // Scales/grid unit 60 +$grid-unit-70: 56px; // Scales/grid unit 70 + + +// -------------------------------------------------------------------------- +// Border Radius +// -------------------------------------------------------------------------- + +$radius-xs: 1px; // radius-xs +$radius-s: 2px; // radius-s - Buttons, inputs +$radius-m: 4px; // radius-m - Focus rings +$radius-l: 8px; // radius-l - Cards, dashboard widgets +$radius-30: 12px; // Radius 30 +$radius-full: 9999px; // radius-full - Pills, avatars, circles + + +// -------------------------------------------------------------------------- +// Gray Scale +// -------------------------------------------------------------------------- +// Neutral colors for backgrounds, borders, and text. + +$gray-100: #f0f0f0; // Scales/Grays/gray-100 - Page background, disabled inputs +$gray-200: #e0e0e0; // Scales/Grays/gray-200 +$gray-300: #dddddd; // Scales/Grays/gray-300 +$gray-400: #cccccc; // Scales/Grays/gray-400 - Disabled borders +$gray-600: #949494; // Scales/Grays/gray-600 - Input borders, disabled text +$gray-700: #757575; // Scales/Grays/gray-700 +$gray-800: #2f2f2f; // Scales/Grays/gray-800 +$gray-900: #1e1e1e; // Scales/Grays/gray-900 - Primary text + +$white: #ffffff; // Scales/Black & White/white + + +// -------------------------------------------------------------------------- +// Theme Colors (Static reference values) +// -------------------------------------------------------------------------- +// For actual theme color usage, use var(--wp-admin-theme-color) instead. +// These are provided for reference and for contexts where CSS vars aren't available. + +$theme-reference: #3858e9; // Scales/Theme/theme (modern scheme) +$theme-darker-10-reference: #2145e6; // Scales/Theme/theme-darker-10 +$theme-darker-20-reference: #183ad6; // Scales/Theme/theme-darker-20 +$theme-alpha-04: rgba(56, 88, 233, 0.04); // Scales/Theme/theme-alpha-04 (4% opacity) +$theme-alpha-08: rgba(56, 88, 233, 0.08); // Scales/Theme/theme-alpha-08 (8% opacity) + +$brand-9: #4465db; // Scales/brand-9 - Focus ring color (static, not theme-dependent) + + +// -------------------------------------------------------------------------- +// Semantic Colors +// -------------------------------------------------------------------------- +// Use these for notices, alerts, and status indicators. +// These are intentionally NOT theme-dependent for consistency. + +$alert-yellow: #f0b849; // Scales/Yellow/alert-yellow - Warnings +$alert-green: #4ab866; // Scales/Green/alert-green - Success +$alert-red: #cc1818; // Scales/Red/alert-red - Errors +$alert-blue: #3858e9; // Info notices (matches modern theme) + +// Background tints for notices +$alert-yellow-bg: #fef8ee; // Warning notice background +$alert-green-bg: #eff9f1; // Success notice background +$alert-red-bg: #fcf0f0; // Error notice background + +$synced-color: #7a00df; // Scales/Purple/--wp-block-synced-color + + +// -------------------------------------------------------------------------- +// Text Colors +// -------------------------------------------------------------------------- + +$text-primary: $gray-900; // Primary text color +$text-secondary: $gray-700; // Secondary text +$text-tertiary: #5d5d5d; // Alias/text/text-tertiary - Placeholder, hints +$text-disabled: $gray-600; // Disabled text + + +// -------------------------------------------------------------------------- +// Component Tokens +// -------------------------------------------------------------------------- + +// Inputs +$input-bg: $white; // Alias/bg/bg-input +$input-border-color: $gray-600; // Default input border +$input-border-color-disabled: $gray-400; +$input-bg-disabled: $gray-100; +$input-border-width-default: 1px; // Input/Default +$input-border-width-focus: 1.5px; // Input/Focus +$field-spacing-horizontal: 8px; // Alias/field-spacing-horizontal + +// Checkboxes and Radios +$checkbox-size: 16px; // Alias/checkbox +$radio-size: 16px; // Alias/radio + +// Toggles +$toggle-width: 32px; // Alias/toggle-width +$toggle-height: 16px; // Alias/toggle-height + +// Buttons +// Note: Gutenberg is transitioning to 40px as the default button size. +// The "compact" size (32px) is available for space-constrained contexts. +$button-height-default: 40px; // Default button height (next-default-40px) +$button-height-compact: 32px; // Compact button height +$button-height-small: 24px; // Small button height + +// Cards and Surfaces +$card-bg: $white; +$card-border-color: rgba(0, 0, 0, 0.1); +$card-border-width: 1px; +$card-border-radius: $radius-l; // 8px for dashboard widgets +$card-border-radius-metabox: 0; // 0 for post editor metaboxes +$card-divider-color: rgba(0, 0, 0, 0.1); + +// Card Padding Sizes +$card-padding-xs: $grid-unit-10; // 8px - xSmall cards +$card-padding-sm: $grid-unit-20; // 16px - Small cards (metaboxes, dashboard widgets) +$card-padding-md-h: $grid-unit-30; // 24px - Medium cards horizontal +$card-padding-md-v: $grid-unit-20; // 16px - Medium cards vertical +$card-padding-lg-h: $grid-unit-40; // 32px - Large cards horizontal +$card-padding-lg-v: $grid-unit-30; // 24px - Large cards vertical + +// Page Layout +$page-padding-large: 48px; // Alias/page-large +$page-padding-small: 24px; // Alias/page-small + + +// -------------------------------------------------------------------------- +// Typography Scale +// -------------------------------------------------------------------------- + +// Font Sizes +$font-size-xs: 11px; // xs - Small labels, button small +$font-size-s: 12px; // s - Body small +$font-size-m: 13px; // m - Base body text, buttons +$font-size-l: 15px; // l - Body large, heading large +$font-size-xl: 20px; // xl - Heading XL + +// Line Heights +$line-height-xs: 16px; // xs +$line-height-s: 20px; // s - Most UI elements +$line-height-m: 24px; // m - Body large + +// Font Weights +$font-weight-regular: 400; // Regular - Body text +$font-weight-medium: 500; // Medium - Headings, buttons + + +// -------------------------------------------------------------------------- +// Elevation (Box Shadows) +// -------------------------------------------------------------------------- + +$elevation-xs: + 0 4px 4px rgba(0, 0, 0, 0.01), + 0 3px 3px rgba(0, 0, 0, 0.02), + 0 1px 2px rgba(0, 0, 0, 0.02), + 0 1px 1px rgba(0, 0, 0, 0.03); + +$elevation-s: + 0 8px 8px rgba(0, 0, 0, 0.02), + 0 1px 2px rgba(0, 0, 0, 0.05); + +$elevation-m: + 0 16px 16px rgba(0, 0, 0, 0.02), + 0 4px 5px rgba(0, 0, 0, 0.03), + 0 2px 3px rgba(0, 0, 0, 0.05); + +$elevation-l: + 0 50px 43px rgba(0, 0, 0, 0.02), + 0 30px 36px rgba(0, 0, 0, 0.04), + 0 15px 27px rgba(0, 0, 0, 0.07), + 0 5px 15px rgba(0, 0, 0, 0.08); + + +// -------------------------------------------------------------------------- +// Layout +// -------------------------------------------------------------------------- + +$modal-width-small: 384px; // Layout/Modal small +$modal-width-medium: 512px; // Layout/Modal medium +$modal-width-large: 840px; // Layout/Modal large + diff --git a/src/wp-admin/css/colors/_variables.scss b/src/wp-admin/css/colors/_variables.scss index 8a073f830e4b6..d37c2b1392f00 100644 --- a/src/wp-admin/css/colors/_variables.scss +++ b/src/wp-admin/css/colors/_variables.scss @@ -1,5 +1,8 @@ @use "sass:color"; +// Import design system tokens +@use "tokens" as *; + // assign default value to all undefined variables $scheme-name: "default" !default; @@ -15,7 +18,7 @@ $notification-color: #d54e21 !default; // global -$body-background: #f1f1f1 !default; +$body-background: $gray-100 !default; $link: #0073aa !default; $link-focus: color.adjust($link, $lightness: 10%) !default; diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index e062a471d7150..393787f8e6c2a 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -631,11 +631,12 @@ code { top: -3px; margin-left: 4px; border: 1px solid #2271b1; - border-radius: 3px; - background: #f6f7f7; + border-radius: 2px; + background: transparent; font-size: 13px; - font-weight: 400; - line-height: 2.15384615; + font-weight: 500; + min-height: 40px; + line-height: 2.92307692; /* 38px for 40px height */ color: #2271b1; /* use the standard color used for buttons */ padding: 0 10px; min-height: 30px; @@ -649,7 +650,6 @@ code { .wrap .add-new-h2:hover, /* deprecated */ .wrap .page-title-action:hover { - background: #f0f0f1; border-color: #0a4b78; color: #0a4b78; } diff --git a/src/wp-admin/css/customize-nav-menus.css b/src/wp-admin/css/customize-nav-menus.css index 26046679ccb1d..3bf0193f02eea 100644 --- a/src/wp-admin/css/customize-nav-menus.css +++ b/src/wp-admin/css/customize-nav-menus.css @@ -672,7 +672,7 @@ #available-menu-items .menu-item-handle.item-added .item-title, #available-menu-items .menu-item-handle.item-added:hover .item-add, #available-menu-items .menu-item-handle.item-added .item-add:focus { - color: #8c8f94; + color: #646970; } #available-menu-items .menu-item-handle.item-added .item-add:before { diff --git a/src/wp-admin/css/forms.css b/src/wp-admin/css/forms.css index 60285a9adc178..719be1aad9ba1 100644 --- a/src/wp-admin/css/forms.css +++ b/src/wp-admin/css/forms.css @@ -209,6 +209,10 @@ input[type="search"]::-webkit-search-decoration { display: none; } +input[type="search"]::-webkit-search-cancel-button { + cursor: pointer; +} + .wp-admin input[type="file"] { padding: 3px 0; cursor: pointer; diff --git a/src/wp-admin/css/themes.css b/src/wp-admin/css/themes.css index ea62a09cf1ed1..113cec87f50e7 100644 --- a/src/wp-admin/css/themes.css +++ b/src/wp-admin/css/themes.css @@ -83,7 +83,7 @@ body.js .theme-browser.search-loading { font-weight: 600; height: 18px; margin: 0; - padding: 15px; + padding: 16px 15px; box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); overflow: hidden; white-space: nowrap; @@ -112,9 +112,26 @@ body.js .theme-browser.search-loading { margin-right: 3px; } +/* Use compact size for space-constrained theme cards */ .theme-browser .theme .theme-actions .button { float: none; margin-left: 3px; + min-height: 32px; + line-height: 2.30769231; /* 30px for 32px min-height */ + padding: 0 12px; +} + +/* Secondary buttons need white background for visibility on semi-transparent overlay */ +.theme-browser .theme .theme-actions .button:not(.button-primary) { + background: #fff; +} + +.theme-browser .theme .theme-actions .button:not(.button-primary):hover { + background: #f0f0f0; +} + +.theme-browser .theme .theme-actions .button:not(.button-primary):focus { + background: #fff; } /** @@ -211,7 +228,7 @@ body.js .theme-browser.search-loading { .theme-browser .theme.active .theme-name { background: #1d2327; color: #fff; - padding-right: 110px; + padding-right: 115px; font-weight: 300; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.5); } @@ -240,7 +257,7 @@ body.js .theme-browser.search-loading { top: 50%; transform: translateY(-50%); right: 0; - padding: 9px 15px; + padding: 9px 12px; box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); } @@ -248,6 +265,19 @@ body.js .theme-browser.search-loading { margin-right: 0; } +/* Active theme secondary buttons need white background for visibility on dark overlay */ +.theme-browser .theme.active .theme-actions .button:not(.button-primary) { + background: #fff; +} + +.theme-browser .theme.active .theme-actions .button:not(.button-primary):hover { + background: #f0f0f0; +} + +.theme-browser .theme.active .theme-actions .button:not(.button-primary):focus { + background: #fff; +} + .theme-browser .theme .theme-author { background: #1d2327; color: #f0f0f1; diff --git a/src/wp-admin/css/view-transitions.css b/src/wp-admin/css/view-transitions.css index bfb556769495b..538a90b502f9e 100644 --- a/src/wp-admin/css/view-transitions.css +++ b/src/wp-admin/css/view-transitions.css @@ -1,7 +1,9 @@ -@view-transition { - navigation: auto; -} +@media (prefers-reduced-motion: no-preference) { + @view-transition { + navigation: auto; + } -#adminmenu > .menu-top { - view-transition-name: attr(id type(), none); + #adminmenu > .menu-top { + view-transition-name: attr(id type(), none); + } } diff --git a/src/wp-admin/includes/ajax-actions.php b/src/wp-admin/includes/ajax-actions.php index 944d0b7d985b2..9850d0543c29b 100644 --- a/src/wp-admin/includes/ajax-actions.php +++ b/src/wp-admin/includes/ajax-actions.php @@ -454,8 +454,8 @@ function wp_ajax_logged_in() { * @since 2.7.0 * @access private * - * @param int $comment_id - * @param int $delta + * @param int $comment_id Comment ID. + * @param int $delta Change in the number of total comments. Default -1. */ function _wp_ajax_delete_comment_response( $comment_id, $delta = -1 ) { $total = isset( $_POST['_total'] ) ? (int) $_POST['_total'] : 0; diff --git a/src/wp-admin/includes/class-wp-importer.php b/src/wp-admin/includes/class-wp-importer.php index 16b08bf42b15a..883be2c218718 100644 --- a/src/wp-admin/includes/class-wp-importer.php +++ b/src/wp-admin/includes/class-wp-importer.php @@ -289,9 +289,10 @@ public function stop_the_insanity() { * Returns value of command line params. * Exits when a required param is not set. * - * @param string $param - * @param bool $required - * @return mixed + * @param string $param The parameter name to retrieve. + * @param bool $required Optional. Whether the parameter is required. Default false. + * @return string|true|null|never The parameter value or true if found, null otherwise. + * The function exits when a required parameter is missing. */ function get_cli_args( $param, $required = false ) { $args = $_SERVER['argv']; diff --git a/src/wp-content/themes/twentyten/404.php b/src/wp-content/themes/twentyten/404.php index 64888d9f12134..8b7b13695a000 100644 --- a/src/wp-content/themes/twentyten/404.php +++ b/src/wp-content/themes/twentyten/404.php @@ -22,9 +22,5 @@ - diff --git a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php index 4aebd1bde02f5..c5643ade65da4 100644 --- a/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php +++ b/src/wp-content/themes/twentytwentyone/classes/class-twenty-twenty-one-dark-mode.php @@ -70,7 +70,7 @@ public function editor_custom_color_variables() { 'twentytwentyone-dark-mode-support-toggle', get_template_directory_uri() . '/assets/js/dark-mode-toggler.js', array(), - '1.0.0', + wp_get_theme()->get( 'Version' ), array( 'in_footer' => true ) ); @@ -78,7 +78,7 @@ public function editor_custom_color_variables() { 'twentytwentyone-editor-dark-mode-support', get_template_directory_uri() . '/assets/js/editor-dark-mode-support.js', array( 'twentytwentyone-dark-mode-support-toggle' ), - '1.0.0', + wp_get_theme()->get( 'Version' ), array( 'in_footer' => true ) ); } @@ -116,7 +116,7 @@ public function customize_controls_enqueue_scripts() { 'twentytwentyone-customize-controls', get_template_directory_uri() . '/assets/js/customize.js', array( 'customize-base', 'customize-controls', 'underscore', 'jquery', 'twentytwentyone-customize-helpers' ), - '1.0.0', + wp_get_theme()->get( 'Version' ), array( 'in_footer' => true ) ); } diff --git a/src/wp-includes/abilities-api/class-wp-ability.php b/src/wp-includes/abilities-api/class-wp-ability.php index 4fa757e55da42..967f1641156b0 100644 --- a/src/wp-includes/abilities-api/class-wp-ability.php +++ b/src/wp-includes/abilities-api/class-wp-ability.php @@ -103,7 +103,7 @@ class WP_Ability { * The ability execute callback. * * @since 6.9.0 - * @var callable( mixed $input= ): (mixed|WP_Error) + * @var callable(mixed): (mixed|WP_Error) */ protected $execute_callback; @@ -111,7 +111,7 @@ class WP_Ability { * The optional ability permission callback. * * @since 6.9.0 - * @var callable( mixed $input= ): (bool|WP_Error) + * @var callable(mixed): (bool|WP_Error) */ protected $permission_callback; diff --git a/src/wp-includes/block-supports/layout.php b/src/wp-includes/block-supports/layout.php index fb46b78ab8f24..bd1badc7a6cd6 100644 --- a/src/wp-includes/block-supports/layout.php +++ b/src/wp-includes/block-supports/layout.php @@ -233,7 +233,7 @@ function wp_register_layout_support( $block_type ) { * @param bool $has_block_gap_support Optional. Whether the theme has support for the block gap. Default false. * @param string|string[]|null $gap_value Optional. The block gap value to apply. Default null. * @param bool $should_skip_gap_serialization Optional. Whether to skip applying the user-defined value set in the editor. Default false. - * @param string $fallback_gap_value Optional. The block gap value to apply. Default '0.5em'. + * @param string|array $fallback_gap_value Optional. The block gap value to apply. If it's an array expected properties are "top" and/or "left". Default '0.5em'. * @param array|null $block_spacing Optional. Custom spacing set on the block. Default null. * @return string CSS styles on success. Else, empty string. */ @@ -427,7 +427,12 @@ function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false foreach ( $gap_sides as $gap_side ) { $process_value = $gap_value; if ( is_array( $gap_value ) ) { - $process_value = $gap_value[ $gap_side ] ?? $fallback_gap_value; + if ( is_array( $fallback_gap_value ) ) { + $fallback_value = $fallback_gap_value[ $gap_side ] ?? reset( $fallback_gap_value ); + } else { + $fallback_value = $fallback_gap_value; + } + $process_value = $gap_value[ $gap_side ] ?? $fallback_value; } // Get spacing CSS variable from preset value if provided. if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) { @@ -490,21 +495,14 @@ function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false } } } elseif ( 'grid' === $layout_type ) { - if ( ! empty( $layout['columnCount'] ) ) { - $layout_styles[] = array( - 'selector' => $selector, - 'declarations' => array( 'grid-template-columns' => 'repeat(' . $layout['columnCount'] . ', minmax(0, 1fr))' ), - ); + /* + * If the gap value is an array, we use the "left" value because it represents the vertical gap, which + * is the relevant one for computation of responsive grid columns. + */ + if ( is_array( $fallback_gap_value ) ) { + $responsive_gap_value = $fallback_gap_value['left'] ?? reset( $fallback_gap_value ); } else { - $minimum_column_width = ! empty( $layout['minimumColumnWidth'] ) ? $layout['minimumColumnWidth'] : '12rem'; - - $layout_styles[] = array( - 'selector' => $selector, - 'declarations' => array( - 'grid-template-columns' => 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))', - 'container-type' => 'inline-size', - ), - ); + $responsive_gap_value = $fallback_gap_value; } if ( $has_block_gap_support && isset( $gap_value ) ) { @@ -514,7 +512,12 @@ function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false foreach ( $gap_sides as $gap_side ) { $process_value = $gap_value; if ( is_array( $gap_value ) ) { - $process_value = $gap_value[ $gap_side ] ?? $fallback_gap_value; + if ( is_array( $fallback_gap_value ) ) { + $fallback_value = $fallback_gap_value[ $gap_side ] ?? reset( $fallback_gap_value ); + } else { + $fallback_value = $fallback_gap_value; + } + $process_value = $gap_value[ $gap_side ] ?? $fallback_value; } // Get spacing CSS variable from preset value if provided. if ( is_string( $process_value ) && str_contains( $process_value, 'var:preset|spacing|' ) ) { @@ -524,14 +527,58 @@ function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false } $combined_gap_value .= "$process_value "; } - $gap_value = trim( $combined_gap_value ); + $gap_value = trim( $combined_gap_value ); + $responsive_gap_value = $gap_value; + } - if ( null !== $gap_value && ! $should_skip_gap_serialization ) { + // Ensure 0 values have a unit so they work in calc(). + if ( '0' === $responsive_gap_value || 0 === $responsive_gap_value ) { + $responsive_gap_value = '0px'; + } + + if ( ! empty( $layout['columnCount'] ) && ! empty( $layout['minimumColumnWidth'] ) ) { + $max_value = 'max(min(' . $layout['minimumColumnWidth'] . ', 100%), (100% - (' . $responsive_gap_value . ' * (' . $layout['columnCount'] . ' - 1))) /' . $layout['columnCount'] . ')'; + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( + 'grid-template-columns' => 'repeat(auto-fill, minmax(' . $max_value . ', 1fr))', + 'container-type' => 'inline-size', + ), + ); + if ( ! empty( $layout['rowCount'] ) ) { $layout_styles[] = array( 'selector' => $selector, - 'declarations' => array( 'gap' => $gap_value ), + 'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout['rowCount'] . ', minmax(1rem, auto))' ), ); } + } elseif ( ! empty( $layout['columnCount'] ) ) { + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'grid-template-columns' => 'repeat(' . $layout['columnCount'] . ', minmax(0, 1fr))' ), + ); + if ( ! empty( $layout['rowCount'] ) ) { + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'grid-template-rows' => 'repeat(' . $layout['rowCount'] . ', minmax(1rem, auto))' ), + ); + } + } else { + $minimum_column_width = ! empty( $layout['minimumColumnWidth'] ) ? $layout['minimumColumnWidth'] : '12rem'; + + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( + 'grid-template-columns' => 'repeat(auto-fill, minmax(min(' . $minimum_column_width . ', 100%), 1fr))', + 'container-type' => 'inline-size', + ), + ); + } + + if ( $has_block_gap_support && null !== $gap_value && ! $should_skip_gap_serialization ) { + $layout_styles[] = array( + 'selector' => $selector, + 'declarations' => array( 'gap' => $gap_value ), + ); } } @@ -568,6 +615,8 @@ function wp_get_layout_style( $selector, $layout, $has_block_gap_support = false * @return string Filtered block content. */ function wp_render_layout_support_flag( $block_content, $block ) { + static $global_styles = null; + $block_type = WP_Block_Type_Registry::get_instance()->get_registered( $block['blockName'] ); $block_supports_layout = block_has_support( $block_type, 'layout', false ) || block_has_support( $block_type, '__experimentalLayout', false ); $child_layout = $block['attrs']['style']['layout'] ?? null; @@ -804,6 +853,18 @@ function wp_render_layout_support_flag( $block_content, $block ) { $block_gap = $global_settings['spacing']['blockGap'] ?? null; $has_block_gap_support = isset( $block_gap ); + // Get default blockGap value from global styles for use in layouts like grid. + // Check block-specific styles first, then fall back to root styles. + $block_name = $block['blockName'] ?? ''; + if ( null === $global_styles ) { + $global_styles = wp_get_global_styles(); + } + $global_block_gap_value = $global_styles['blocks'][ $block_name ]['spacing']['blockGap'] ?? ( $global_styles['spacing']['blockGap'] ?? null ); + + if ( null !== $global_block_gap_value ) { + $fallback_gap_value = $global_block_gap_value; + } + /* * Generates a unique ID based on all the data required to obtain the * corresponding layout style. Keeps the CSS class names the same diff --git a/src/wp-includes/blocks.php b/src/wp-includes/blocks.php index e2c594d7ecfc6..2a9968608106a 100644 --- a/src/wp-includes/blocks.php +++ b/src/wp-includes/blocks.php @@ -2421,10 +2421,6 @@ function parse_blocks( $content ) { */ $parser_class = apply_filters( 'block_parser_class', 'WP_Block_Parser' ); - if ( ! class_exists( $parser_class ) ) { - return array(); - } - $parser = new $parser_class(); return $parser->parse( $content ); } diff --git a/src/wp-includes/class-wp-block-parser-block.php b/src/wp-includes/class-wp-block-parser-block.php new file mode 100644 index 0000000000000..97dd687c1ffe1 --- /dev/null +++ b/src/wp-includes/class-wp-block-parser-block.php @@ -0,0 +1,90 @@ + 3 ) + * + * @since 5.0.0 + * @var array|null + */ + public $attrs; + + /** + * List of inner blocks (of this same class) + * + * @since 5.0.0 + * @var WP_Block_Parser_Block[] + */ + public $innerBlocks; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + + /** + * Resultant HTML from inside block comment delimiters + * after removing inner blocks + * + * @example "...Just testing..." -> "Just testing..." + * + * @since 5.0.0 + * @var string + */ + public $innerHTML; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + + /** + * List of string fragments and null markers where inner blocks were found + * + * @example array( + * 'innerHTML' => 'BeforeInnerAfter', + * 'innerBlocks' => array( block, block ), + * 'innerContent' => array( 'Before', null, 'Inner', null, 'After' ), + * ) + * + * @since 5.0.0 + * @var array + */ + public $innerContent; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + + /** + * Constructor. + * + * Will populate object properties from the provided arguments. + * + * @since 5.0.0 + * + * @param string $name Name of block. + * @param array $attrs Optional set of attributes from block comment delimiters. + * @param array $inner_blocks List of inner blocks (of this same class). + * @param string $inner_html Resultant HTML from inside block comment delimiters after removing inner blocks. + * @param array $inner_content List of string fragments and null markers where inner blocks were found. + */ + public function __construct( $name, $attrs, $inner_blocks, $inner_html, $inner_content ) { + $this->blockName = $name; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + $this->attrs = $attrs; + $this->innerBlocks = $inner_blocks; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + $this->innerHTML = $inner_html; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + $this->innerContent = $inner_content; // phpcs:ignore WordPress.NamingConventions.ValidVariableName + } +} diff --git a/src/wp-includes/class-wp-block-parser-frame.php b/src/wp-includes/class-wp-block-parser-frame.php new file mode 100644 index 0000000000000..6ab5dd3087dfb --- /dev/null +++ b/src/wp-includes/class-wp-block-parser-frame.php @@ -0,0 +1,79 @@ +block = $block; + $this->token_start = $token_start; + $this->token_length = $token_length; + $this->prev_offset = isset( $prev_offset ) ? $prev_offset : $token_start + $token_length; + $this->leading_html_start = $leading_html_start; + } +} diff --git a/src/wp-includes/class-wp-block-parser.php b/src/wp-includes/class-wp-block-parser.php new file mode 100644 index 0000000000000..bf8a59249d99d --- /dev/null +++ b/src/wp-includes/class-wp-block-parser.php @@ -0,0 +1,404 @@ +This is inside a block!" + * + * @since 5.0.0 + * @var string + */ + public $document; + + /** + * Tracks parsing progress through document + * + * @since 5.0.0 + * @var int + */ + public $offset; + + /** + * List of parsed blocks + * + * @since 5.0.0 + * @var array[] + */ + public $output; + + /** + * Stack of partially-parsed structures in memory during parse + * + * @since 5.0.0 + * @var WP_Block_Parser_Frame[] + */ + public $stack; + + /** + * Parses a document and returns a list of block structures + * + * When encountering an invalid parse will return a best-effort + * parse. In contrast to the specification parser this does not + * return an error on invalid inputs. + * + * @since 5.0.0 + * + * @param string $document Input document being parsed. + * @return array[] + */ + public function parse( $document ) { + $this->document = $document; + $this->offset = 0; + $this->output = array(); + $this->stack = array(); + + while ( $this->proceed() ) { + continue; + } + + return $this->output; + } + + /** + * Processes the next token from the input document + * and returns whether to proceed eating more tokens + * + * This is the "next step" function that essentially + * takes a token as its input and decides what to do + * with that token before descending deeper into a + * nested block tree or continuing along the document + * or breaking out of a level of nesting. + * + * @internal + * @since 5.0.0 + * @return bool + */ + public function proceed() { + $next_token = $this->next_token(); + list( $token_type, $block_name, $attrs, $start_offset, $token_length ) = $next_token; + $stack_depth = count( $this->stack ); + + // we may have some HTML soup before the next block. + $leading_html_start = $start_offset > $this->offset ? $this->offset : null; + + switch ( $token_type ) { + case 'no-more-tokens': + // if not in a block then flush output. + if ( 0 === $stack_depth ) { + $this->add_freeform(); + return false; + } + + /* + * Otherwise we have a problem + * This is an error + * + * we have options + * - treat it all as freeform text + * - assume an implicit closer (easiest when not nesting) + */ + + // for the easy case we'll assume an implicit closer. + if ( 1 === $stack_depth ) { + $this->add_block_from_stack(); + return false; + } + + /* + * for the nested case where it's more difficult we'll + * have to assume that multiple closers are missing + * and so we'll collapse the whole stack piecewise + */ + while ( 0 < count( $this->stack ) ) { + $this->add_block_from_stack(); + } + return false; + + case 'void-block': + /* + * easy case is if we stumbled upon a void block + * in the top-level of the document + */ + if ( 0 === $stack_depth ) { + if ( isset( $leading_html_start ) ) { + $this->output[] = (array) $this->freeform( + substr( + $this->document, + $leading_html_start, + $start_offset - $leading_html_start + ) + ); + } + + $this->output[] = (array) new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ); + $this->offset = $start_offset + $token_length; + return true; + } + + // otherwise we found an inner block. + $this->add_inner_block( + new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ), + $start_offset, + $token_length + ); + $this->offset = $start_offset + $token_length; + return true; + + case 'block-opener': + // track all newly-opened blocks on the stack. + array_push( + $this->stack, + new WP_Block_Parser_Frame( + new WP_Block_Parser_Block( $block_name, $attrs, array(), '', array() ), + $start_offset, + $token_length, + $start_offset + $token_length, + $leading_html_start + ) + ); + $this->offset = $start_offset + $token_length; + return true; + + case 'block-closer': + /* + * if we're missing an opener we're in trouble + * This is an error + */ + if ( 0 === $stack_depth ) { + /* + * we have options + * - assume an implicit opener + * - assume _this_ is the opener + * - give up and close out the document + */ + $this->add_freeform(); + return false; + } + + // if we're not nesting then this is easy - close the block. + if ( 1 === $stack_depth ) { + $this->add_block_from_stack( $start_offset ); + $this->offset = $start_offset + $token_length; + return true; + } + + /* + * otherwise we're nested and we have to close out the current + * block and add it as a new innerBlock to the parent + */ + $stack_top = array_pop( $this->stack ); + $html = substr( $this->document, $stack_top->prev_offset, $start_offset - $stack_top->prev_offset ); + $stack_top->block->innerHTML .= $html; + $stack_top->block->innerContent[] = $html; + $stack_top->prev_offset = $start_offset + $token_length; + + $this->add_inner_block( + $stack_top->block, + $stack_top->token_start, + $stack_top->token_length, + $start_offset + $token_length + ); + $this->offset = $start_offset + $token_length; + return true; + + default: + // This is an error. + $this->add_freeform(); + return false; + } + } + + /** + * Scans the document from where we last left off + * and finds the next valid token to parse if it exists + * + * Returns the type of the find: kind of find, block information, attributes + * + * @internal + * @since 5.0.0 + * @since 4.6.1 fixed a bug in attribute parsing which caused catastrophic backtracking on invalid block comments + * @return array + */ + public function next_token() { + $matches = null; + + /* + * aye the magic + * we're using a single RegExp to tokenize the block comment delimiters + * we're also using a trick here because the only difference between a + * block opener and a block closer is the leading `/` before `wp:` (and + * a closer has no attributes). we can trap them both and process the + * match back in PHP to see which one it was. + */ + $has_match = preg_match( + '/).)*+)?}\s+)?(?P\/)?-->/s', + $this->document, + $matches, + PREG_OFFSET_CAPTURE, + $this->offset + ); + + // if we get here we probably have catastrophic backtracking or out-of-memory in the PCRE. + if ( false === $has_match ) { + return array( 'no-more-tokens', null, null, null, null ); + } + + // we have no more tokens. + if ( 0 === $has_match ) { + return array( 'no-more-tokens', null, null, null, null ); + } + + list( $match, $started_at ) = $matches[0]; + + $length = strlen( $match ); + $is_closer = isset( $matches['closer'] ) && -1 !== $matches['closer'][1]; + $is_void = isset( $matches['void'] ) && -1 !== $matches['void'][1]; + $namespace = $matches['namespace']; + $namespace = ( isset( $namespace ) && -1 !== $namespace[1] ) ? $namespace[0] : 'core/'; + $name = $namespace . $matches['name'][0]; + $has_attrs = isset( $matches['attrs'] ) && -1 !== $matches['attrs'][1]; + + /* + * Fun fact! It's not trivial in PHP to create "an empty associative array" since all arrays + * are associative arrays. If we use `array()` we get a JSON `[]` + */ + $attrs = $has_attrs + ? json_decode( $matches['attrs'][0], /* as-associative */ true ) + : array(); + + /* + * This state isn't allowed + * This is an error + */ + if ( $is_closer && ( $is_void || $has_attrs ) ) { + // we can ignore them since they don't hurt anything. + } + + if ( $is_void ) { + return array( 'void-block', $name, $attrs, $started_at, $length ); + } + + if ( $is_closer ) { + return array( 'block-closer', $name, null, $started_at, $length ); + } + + return array( 'block-opener', $name, $attrs, $started_at, $length ); + } + + /** + * Returns a new block object for freeform HTML + * + * @internal + * @since 5.0.0 + * + * @param string $inner_html HTML content of block. + * @return WP_Block_Parser_Block freeform block object. + */ + public function freeform( $inner_html ) { + return new WP_Block_Parser_Block( null, array(), array(), $inner_html, array( $inner_html ) ); + } + + /** + * Pushes a length of text from the input document + * to the output list as a freeform block. + * + * @internal + * @since 5.0.0 + * @param null $length how many bytes of document text to output. + */ + public function add_freeform( $length = null ) { + $length = $length ? $length : strlen( $this->document ) - $this->offset; + + if ( 0 === $length ) { + return; + } + + $this->output[] = (array) $this->freeform( substr( $this->document, $this->offset, $length ) ); + } + + /** + * Given a block structure from memory pushes + * a new block to the output list. + * + * @internal + * @since 5.0.0 + * @param WP_Block_Parser_Block $block The block to add to the output. + * @param int $token_start Byte offset into the document where the first token for the block starts. + * @param int $token_length Byte length of entire block from start of opening token to end of closing token. + * @param int|null $last_offset Last byte offset into document if continuing form earlier output. + */ + public function add_inner_block( WP_Block_Parser_Block $block, $token_start, $token_length, $last_offset = null ) { + $parent = $this->stack[ count( $this->stack ) - 1 ]; + $parent->block->innerBlocks[] = (array) $block; + $html = substr( $this->document, $parent->prev_offset, $token_start - $parent->prev_offset ); + + if ( ! empty( $html ) ) { + $parent->block->innerHTML .= $html; + $parent->block->innerContent[] = $html; + } + + $parent->block->innerContent[] = null; + $parent->prev_offset = $last_offset ? $last_offset : $token_start + $token_length; + } + + /** + * Pushes the top block from the parsing stack to the output list. + * + * @internal + * @since 5.0.0 + * @param int|null $end_offset byte offset into document for where we should stop sending text output as HTML. + */ + public function add_block_from_stack( $end_offset = null ) { + $stack_top = array_pop( $this->stack ); + $prev_offset = $stack_top->prev_offset; + + $html = isset( $end_offset ) + ? substr( $this->document, $prev_offset, $end_offset - $prev_offset ) + : substr( $this->document, $prev_offset ); + + if ( ! empty( $html ) ) { + $stack_top->block->innerHTML .= $html; + $stack_top->block->innerContent[] = $html; + } + + if ( isset( $stack_top->leading_html_start ) ) { + $this->output[] = (array) $this->freeform( + substr( + $this->document, + $stack_top->leading_html_start, + $stack_top->token_start - $stack_top->leading_html_start + ) + ); + } + + $this->output[] = (array) $stack_top->block; + } +} + +/** + * WP_Block_Parser_Block class. + * + * Required for backward compatibility in WordPress Core. + */ +require_once __DIR__ . '/class-wp-block-parser-block.php'; + +/** + * WP_Block_Parser_Frame class. + * + * Required for backward compatibility in WordPress Core. + */ +require_once __DIR__ . '/class-wp-block-parser-frame.php'; diff --git a/src/wp-includes/class-wp-block-processor.php b/src/wp-includes/class-wp-block-processor.php index 39f79b99e79a4..7b20fbf85d3bf 100644 --- a/src/wp-includes/class-wp-block-processor.php +++ b/src/wp-includes/class-wp-block-processor.php @@ -1295,6 +1295,17 @@ public function extract_full_block_and_advance(): ?array { $block['innerBlocks'][] = $inner_block; $block['innerContent'][] = null; } + + /* + * Because the parser has advanced past the closing block token, it + * may be matched on an HTML span. This needs to be processed before + * moving on to the next token at the start of the next loop iteration. + */ + if ( $this->is_html() ) { + $chunk = $this->get_html_content(); + $block['innerHTML'] .= $chunk; + $block['innerContent'][] = $chunk; + } } return $block; diff --git a/src/wp-includes/css/buttons.css b/src/wp-includes/css/buttons.css index 5146be4274254..3eda51ae5d788 100644 --- a/src/wp-includes/css/buttons.css +++ b/src/wp-includes/css/buttons.css @@ -46,15 +46,16 @@ TABLE OF CONTENTS: display: inline-block; text-decoration: none; font-size: 13px; - line-height: 2.15384615; /* 28px */ - min-height: 30px; + font-weight: 500; + line-height: 2.92307692; /* 38px - allows 40px min-height with 1px border */ + min-height: 40px; margin: 0; - padding: 0 10px; + padding: 0 16px; cursor: pointer; border-width: 1px; border-style: solid; -webkit-appearance: none; - border-radius: 3px; + border-radius: 2px; white-space: nowrap; box-sizing: border-box; } @@ -69,26 +70,36 @@ TABLE OF CONTENTS: padding: 0; } -.wp-core-ui .button.button-large, -.wp-core-ui .button-group.button-large .button { +/* Compact size - 32px, for space-constrained contexts */ +.wp-core-ui .button.button-compact, +.wp-core-ui .button-group.button-compact .button { + line-height: 2.30769231; /* 30px - allows 32px min-height with 1px border */ min-height: 32px; - line-height: 2.30769231; /* 30px */ padding: 0 12px; } +/* Small size - 24px */ .wp-core-ui .button.button-small, .wp-core-ui .button-group.button-small .button { - min-height: 26px; - line-height: 2.18181818; /* 24px */ + line-height: 2; /* 22px - allows 24px min-height with 1px border */ + min-height: 24px; padding: 0 8px; font-size: 11px; } +/* Large size - explicit 40px (same as default, for semantic clarity) */ +.wp-core-ui .button.button-large, +.wp-core-ui .button-group.button-large .button { + line-height: 2.92307692; /* 38px - allows 40px min-height with 1px border */ + min-height: 40px; + padding: 0 16px; +} + .wp-core-ui .button.button-hero, .wp-core-ui .button-group.button-hero .button { font-size: 14px; - min-height: 46px; - line-height: 3.14285714; + line-height: 3.28571429; /* 46px - allows 48px min-height with 1px border */ + min-height: 48px; padding: 0 36px; } @@ -115,9 +126,9 @@ TABLE OF CONTENTS: .wp-core-ui .button, .wp-core-ui .button-secondary { - color: #2271b1; - border-color: #2271b1; - background: #f6f7f7; + color: #3858e9; + border-color: #3858e9; + background: transparent; vertical-align: top; } @@ -127,21 +138,21 @@ TABLE OF CONTENTS: .wp-core-ui .button.hover, .wp-core-ui .button:hover, -.wp-core-ui .button-secondary:hover{ - background: #f0f0f1; - border-color: #0a4b78; - color: #0a4b78; +.wp-core-ui .button-secondary:hover { + background: rgba(56, 88, 233, 0.04); + border-color: #183ad6; + color: #183ad6; } .wp-core-ui .button.focus, .wp-core-ui .button:focus, .wp-core-ui .button-secondary:focus { - background: #f6f7f7; - border-color: #3582c4; - color: #0a4b78; - box-shadow: 0 0 0 1px #3582c4; + background: transparent; + border-color: #3858e9; + color: #3858e9; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) #3858e9; /* Only visible in Windows High Contrast mode */ - outline: 2px solid transparent; + outline: 1px solid transparent; /* Reset inherited offset from Gutenberg */ outline-offset: 0; } @@ -149,25 +160,24 @@ TABLE OF CONTENTS: /* :active state */ .wp-core-ui .button:active, .wp-core-ui .button-secondary:active { - background: #f6f7f7; - border-color: #8c8f94; + background: rgba(56, 88, 233, 0.08); + border-color: #183ad6; + color: #183ad6; box-shadow: none; } /* pressed state e.g. a selected setting */ .wp-core-ui .button.active, .wp-core-ui .button.active:hover { - background-color: #dcdcde; - color: #135e96; - border-color: #0a4b78; - box-shadow: inset 0 2px 5px -3px #0a4b78; + background-color: rgba(56, 88, 233, 0.04); + color: #3858e9; + border-color: #3858e9; + box-shadow: none; } .wp-core-ui .button.active:focus { - border-color: #3582c4; - box-shadow: - inset 0 2px 5px -3px #0a4b78, - 0 0 0 1px #3582c4; + border-color: #3858e9; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) #3858e9; } .wp-core-ui .button[disabled], @@ -177,9 +187,9 @@ TABLE OF CONTENTS: .wp-core-ui .button-secondary:disabled, .wp-core-ui .button-secondary.disabled, .wp-core-ui .button-disabled { - color: #a7aaad !important; - border-color: #dcdcde !important; - background: #f6f7f7 !important; + color: #949494 !important; + border-color: #dddddd !important; + background: transparent !important; box-shadow: none !important; cursor: default; transform: none !important; @@ -201,7 +211,7 @@ TABLE OF CONTENTS: cursor: pointer; text-align: left; /* Mimics the default link style in common.css */ - color: #2271b1; + color: #3858e9; text-decoration: underline; transition-property: border, background, color; transition-duration: .05s; @@ -210,14 +220,15 @@ TABLE OF CONTENTS: .wp-core-ui .button-link:hover, .wp-core-ui .button-link:active { - color: #135e96; + color: #183ad6; } .wp-core-ui .button-link:focus { - color: #043959; - box-shadow: 0 0 0 2px #2271b1; + color: #3858e9; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) #3858e9; + border-radius: 2px; /* Only visible in Windows High Contrast mode */ - outline: 2px solid transparent; + outline: 1px solid transparent; } .wp-core-ui .button-link-delete { @@ -241,35 +252,37 @@ TABLE OF CONTENTS: ---------------------------------------------------------------------------- */ .wp-core-ui .button-primary { - background: #2271b1; - border-color: #2271b1; + background: #3858e9; + border-color: #3858e9; color: #fff; text-decoration: none; text-shadow: none; } .wp-core-ui .button-primary.hover, -.wp-core-ui .button-primary:hover, -.wp-core-ui .button-primary.focus, -.wp-core-ui .button-primary:focus { - background: #135e96; - border-color: #135e96; +.wp-core-ui .button-primary:hover { + background: #2145e6; + border-color: #2145e6; color: #fff; } .wp-core-ui .button-primary.focus, .wp-core-ui .button-primary:focus { + background: #3858e9; + border-color: #3858e9; + color: #fff; box-shadow: - 0 0 0 1px #fff, - 0 0 0 3px #2271b1; + 0 0 0 var(--wp-admin-border-width-focus, 1.5px) #3858e9, + inset 0 0 0 1px #fff; + outline: 1px solid transparent; } .wp-core-ui .button-primary.active, .wp-core-ui .button-primary.active:hover, .wp-core-ui .button-primary.active:focus, .wp-core-ui .button-primary:active { - background: #135e96; - border-color: #135e96; + background: #183ad6; + border-color: #183ad6; box-shadow: none; color: #fff; } @@ -278,9 +291,9 @@ TABLE OF CONTENTS: .wp-core-ui .button-primary:disabled, .wp-core-ui .button-primary-disabled, .wp-core-ui .button-primary.disabled { - color: #a7aaad !important; - background: #f6f7f7 !important; - border-color: #dcdcde !important; + color: #949494 !important; + background: #f0f0f0 !important; + border-color: #f0f0f0 !important; box-shadow: none !important; text-shadow: none !important; cursor: default; @@ -309,11 +322,11 @@ TABLE OF CONTENTS: } .wp-core-ui .button-group > .button:first-child { - border-radius: 3px 0 0 3px; + border-radius: 2px 0 0 2px; } .wp-core-ui .button-group > .button:last-child { - border-radius: 0 3px 3px 0; + border-radius: 0 2px 2px 0; } .wp-core-ui .button-group > .button-primary + .button { @@ -353,7 +366,7 @@ TABLE OF CONTENTS: input#save-post, a.preview { padding: 0 14px; - line-height: 2.71428571; /* 38px */ + line-height: 2.71428571; /* 38px - allows 40px min-height with 1px border */ font-size: 14px; vertical-align: middle; min-height: 40px; @@ -366,9 +379,9 @@ TABLE OF CONTENTS: } #media-upload.wp-core-ui .button { - padding: 0 10px 1px; + padding: 0 10px; + line-height: 1.69230769; /* 22px */ min-height: 24px; - line-height: 22px; font-size: 13px; } @@ -386,8 +399,8 @@ TABLE OF CONTENTS: .wp-core-ui.wp-customizer .button { font-size: 13px; - line-height: 2.15384615; /* 28px */ - min-height: 30px; + line-height: 2.30769231; /* 30px */ + min-height: 32px; margin: 0; vertical-align: inherit; } @@ -404,9 +417,9 @@ TABLE OF CONTENTS: /* Reset responsive styles on Log in button on iframed login form */ .interim-login .button.button-large { - min-height: 30px; - line-height: 2; - padding: 0 12px 2px; + min-height: 32px; + line-height: 2.30769231; /* 30px */ + padding: 0 12px; } } diff --git a/src/wp-includes/formatting.php b/src/wp-includes/formatting.php index b3a5a1ca135b4..f59f877775b77 100644 --- a/src/wp-includes/formatting.php +++ b/src/wp-includes/formatting.php @@ -5227,11 +5227,6 @@ function wp_pre_kses_less_than_callback( $matches ) { * @return string Filtered text to run through KSES. */ function wp_pre_kses_block_attributes( $content, $allowed_html, $allowed_protocols ) { - // If the block parser isn't available, skip block attribute filtering. - if ( ! class_exists( 'WP_Block_Parser' ) ) { - return $content; - } - /* * `filter_block_content` is expected to call `wp_kses`. Temporarily remove * the filter to avoid recursion. diff --git a/src/wp-includes/kses.php b/src/wp-includes/kses.php index fd489c06c71f2..c71453177ddc2 100644 --- a/src/wp-includes/kses.php +++ b/src/wp-includes/kses.php @@ -1623,6 +1623,11 @@ function wp_kses_hair( $attr, $allowed_protocols ) { $processor = new WP_HTML_Tag_Processor( "" ); $processor->next_token(); + $attribute_names = $processor->get_attribute_names_with_prefix( '' ); + if ( null === $attribute_names || 0 === count( $attribute_names ) ) { + return $attributes; + } + $syntax_characters = array( '&' => '&', '<' => '<', @@ -1631,7 +1636,7 @@ function wp_kses_hair( $attr, $allowed_protocols ) { '"' => '"', ); - foreach ( $processor->get_attribute_names_with_prefix( '' ) as $name ) { + foreach ( $attribute_names as $name ) { $value = $processor->get_attribute( $name ); $is_bool = true === $value; if ( is_string( $value ) && in_array( $name, $uris, true ) ) { diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 6933ad69957e2..5d350b1a18951 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -1785,7 +1785,7 @@ function wp_image_add_srcset_and_sizes( $image, $image_meta, $attachment_id ) { } // Bail early if an image has been inserted and later edited. - if ( preg_match( '/-e[0-9]{13}/', $image_meta['file'], $img_edit_hash ) + if ( isset( $image_meta['file'] ) && preg_match( '/-e[0-9]{13}/', $image_meta['file'], $img_edit_hash ) && ! str_contains( wp_basename( $image_src ), $img_edit_hash[0] ) ) { return $image; diff --git a/src/wp-includes/style-engine/class-wp-style-engine-css-declarations.php b/src/wp-includes/style-engine/class-wp-style-engine-css-declarations.php index fbcb54b73aa3e..796ea86c207a4 100644 --- a/src/wp-includes/style-engine/class-wp-style-engine-css-declarations.php +++ b/src/wp-includes/style-engine/class-wp-style-engine-css-declarations.php @@ -59,6 +59,11 @@ public function add_declaration( $property, $value ) { return $this; } + // Bail early if value is not a string. Prevents fatal errors from malformed block markup. + if ( ! is_string( $value ) ) { + return $this; + } + // Trims the value. If empty, bail early. $value = trim( $value ); if ( '' === $value ) { diff --git a/src/wp-settings.php b/src/wp-settings.php index 207a69e258247..60c220100f539 100644 --- a/src/wp-settings.php +++ b/src/wp-settings.php @@ -377,15 +377,9 @@ require ABSPATH . WPINC . '/class-wp-block.php'; require ABSPATH . WPINC . '/class-wp-block-list.php'; require ABSPATH . WPINC . '/class-wp-block-metadata-registry.php'; -if ( file_exists( ABSPATH . WPINC . '/class-wp-block-parser-block.php' ) ) { - require ABSPATH . WPINC . '/class-wp-block-parser-block.php'; -} -if ( file_exists( ABSPATH . WPINC . '/class-wp-block-parser-frame.php' ) ) { - require ABSPATH . WPINC . '/class-wp-block-parser-frame.php'; -} -if ( file_exists( ABSPATH . WPINC . '/class-wp-block-parser.php' ) ) { - require ABSPATH . WPINC . '/class-wp-block-parser.php'; -} +require ABSPATH . WPINC . '/class-wp-block-parser-block.php'; +require ABSPATH . WPINC . '/class-wp-block-parser-frame.php'; +require ABSPATH . WPINC . '/class-wp-block-parser.php'; require ABSPATH . WPINC . '/class-wp-classic-to-block-menu-converter.php'; require ABSPATH . WPINC . '/class-wp-navigation-fallback.php'; require ABSPATH . WPINC . '/block-bindings.php'; diff --git a/tests/performance/log-results.js b/tests/performance/log-results.js index f8214b68d4eb3..a3ef4003fab6c 100644 --- a/tests/performance/log-results.js +++ b/tests/performance/log-results.js @@ -10,7 +10,6 @@ /** * External dependencies. */ -const https = require( 'https' ); const [ token, branch, hash, baseHash, date, host ] = process.argv.slice( 2 ); const { median, parseFile, accumulateValues } = require( './utils' ); @@ -82,40 +81,40 @@ for ( const { title, results } of afterStats ) { } } -const data = new TextEncoder().encode( - JSON.stringify( { - branch, - hash, - baseHash, - timestamp: date, - metrics: metrics, - baseMetrics: baseMetrics, - } ) -); - -const options = { - hostname: host, - port: 443, - path: '/api/log?token=' + token, - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'Content-Length': data.length, - }, -}; - -const req = https.request( options, ( res ) => { - console.log( `statusCode: ${ res.statusCode }` ); - - res.on( 'data', ( d ) => { - process.stdout.write( d ); - } ); +const data = JSON.stringify( { + branch, + hash, + baseHash, + timestamp: date, + metrics: metrics, + baseMetrics: baseMetrics, } ); -req.on( 'error', ( error ) => { - console.error( error ); - process.exit( 1 ); -} ); - -req.write( data ); -req.end(); +( async () => { + try { + const response = await fetch( + `https://${ host }/api/log?token=${ token }`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: data, + } + ); + + console.log( `statusCode: ${ response.status }` ); + + const responseText = await response.text(); + if ( responseText ) { + console.log( responseText ); + } + + if ( ! response.ok ) { + process.exit( 1 ); + } + } catch ( error ) { + console.error( error ); + process.exit( 1 ); + } +} )(); diff --git a/tests/phpunit/data/blocks/fixtures/core__columns.server.html b/tests/phpunit/data/blocks/fixtures/core__columns.server.html index 02d855cbd6c38..5b5faf4f3d7a4 100644 --- a/tests/phpunit/data/blocks/fixtures/core__columns.server.html +++ b/tests/phpunit/data/blocks/fixtures/core__columns.server.html @@ -1,5 +1,5 @@ -
+
diff --git a/tests/phpunit/data/blocks/fixtures/core__columns__deprecated.server.html b/tests/phpunit/data/blocks/fixtures/core__columns__deprecated.server.html index 6b695d15963de..b19349744dfbc 100644 --- a/tests/phpunit/data/blocks/fixtures/core__columns__deprecated.server.html +++ b/tests/phpunit/data/blocks/fixtures/core__columns__deprecated.server.html @@ -1,5 +1,5 @@ -
+

Column One, Paragraph One

diff --git a/tests/phpunit/tests/block-processor/wpBlockProcessor.php b/tests/phpunit/tests/block-processor/wpBlockProcessor.php index 021288f9cb87f..6f8934e003030 100644 --- a/tests/phpunit/tests/block-processor/wpBlockProcessor.php +++ b/tests/phpunit/tests/block-processor/wpBlockProcessor.php @@ -1296,6 +1296,132 @@ public function test_scans_directly_to_requested_block_type( string $html, strin ); } + /** + * Ensures that block extraction matches the behavior of the default block parser. + * + * @ticket 64537 + * + * @dataProvider data_various_block_posts + * + * @param string $test_document An HTML document to parse as blocks. + */ + public function test_extracts_equivalent_parses_as_parse_blocks( string $test_document ) { + $processor = new WP_Block_Processor( $test_document ); + $blocks = array(); + + while ( $processor->next_block( '*' ) ) { + $blocks[] = $processor->extract_full_block_and_advance(); + } + + $this->assertSame( + parse_blocks( $test_document ), + $blocks, + 'Failed to properly parse the block structure.' + ); + } + + /** + * Data provider. + * + * @return Generator + */ + public static function data_various_block_posts() { + yield 'Empty post' => array( '' ); + + yield 'Void block' => array( '' ); + + yield 'Empty block' => array( '' ); + + yield 'Paragraph block' => array( '

Test

' ); + + yield 'Paragraph block with attributes' => array( + '

Test

', + ); + + yield 'Group with void inner' => array( + '', + ); + + /* + * @todo There is a hidden bug in here, which is possibly a problem in + * the default parser. There are HTML spans of newlines between + * these block delimiters, and without them, the parse doesn’t + * match `parse_blocks()`. However, `parse_blocks()` is inconsistent + * in its behavior. Whereas it produces an empty text chunk here, + * in the case of a void inner block it produces none. The test is + * being adjusted to step around this issue so that it can be resolved + * separately, and until it’s clear if there is an implementation issue + * with `parse_blocks()` itself. + */ + yield 'Empty columns' => array( + << + + + +HTML + , + ); + + yield 'Contentful columns' => array( + << +
    + +
  • A good point.
  • + +
+ +HTML + , + ); + + yield 'Group with mixed content' => array( + << +
+

Test

+ This is freeform. + + End + +
That’s it!
+ + +
+ +HTML + , + ); + + yield 'Nested blocks' => array( + << +
+ + + +
+ +HTML + , + ); + + yield 'Attributes on nested blocks' => array( + << + + + + + + + +HTML + , + ); + } + /** * Data provider. * diff --git a/tests/phpunit/tests/block-supports/layout.php b/tests/phpunit/tests/block-supports/layout.php index 3b71b2c4b7482..8661077df8662 100644 --- a/tests/phpunit/tests/block-supports/layout.php +++ b/tests/phpunit/tests/block-supports/layout.php @@ -293,7 +293,7 @@ public function data_layout_support_flag_renders_classnames_on_wrapper() { ), ), ), - 'expected_output' => '
', + 'expected_output' => '
', ), 'single wrapper block layout with grid type' => array( 'args' => array( @@ -312,7 +312,7 @@ public function data_layout_support_flag_renders_classnames_on_wrapper() { ), ), ), - 'expected_output' => '
', + 'expected_output' => '
', ), 'skip classname output if block does not support layout and there are no child layout classes to be output' => array( 'args' => array( @@ -542,9 +542,19 @@ public function test_layout_support_flag_renders_consistent_container_hash( $blo $processor = new WP_HTML_Tag_Processor( $output ); $processor->next_tag(); - $this->assertTrue( - $processor->has_class( $expected_class ), - "Expected class '$expected_class' not found in the rendered output, probably because of a different hash." + // Extract the actual container class from the output for better error messages. + $actual_class = ''; + foreach ( $processor->class_list() as $class_name ) { + if ( str_starts_with( $class_name, 'wp-container-core-group-is-layout-' ) ) { + $actual_class = $class_name; + break; + } + } + + $this->assertEquals( + $expected_class, + $actual_class, + 'Expected class not found in the rendered output, probably because of a different hash.' ); } @@ -566,7 +576,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { ), ), ), - 'expected_class' => 'wp-container-core-group-is-layout-c5c7d83f', + 'expected_class' => 'wp-container-core-group-is-layout-a6248535', ), 'default type block gap 24px' => array( 'block_attributes' => array( @@ -579,7 +589,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { ), ), ), - 'expected_class' => 'wp-container-core-group-is-layout-634f0b9d', + 'expected_class' => 'wp-container-core-group-is-layout-61b496ee', ), 'constrained type justified left' => array( 'block_attributes' => array( @@ -588,7 +598,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { 'justifyContent' => 'left', ), ), - 'expected_class' => 'wp-container-core-group-is-layout-12dd3699', + 'expected_class' => 'wp-container-core-group-is-layout-54d22900', ), 'constrained type justified right' => array( 'block_attributes' => array( @@ -597,7 +607,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { 'justifyContent' => 'right', ), ), - 'expected_class' => 'wp-container-core-group-is-layout-f1f2ed93', + 'expected_class' => 'wp-container-core-group-is-layout-2910ada7', ), 'flex type horizontal' => array( 'block_attributes' => array( @@ -607,7 +617,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { 'flexWrap' => 'nowrap', ), ), - 'expected_class' => 'wp-container-core-group-is-layout-2487dcaa', + 'expected_class' => 'wp-container-core-group-is-layout-f5d79bea', ), 'flex type vertical' => array( 'block_attributes' => array( @@ -616,7 +626,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { 'orientation' => 'vertical', ), ), - 'expected_class' => 'wp-container-core-group-is-layout-fe9cc265', + 'expected_class' => 'wp-container-core-group-is-layout-2c90304e', ), 'grid type' => array( 'block_attributes' => array( @@ -624,7 +634,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { 'type' => 'grid', ), ), - 'expected_class' => 'wp-container-core-group-is-layout-478b6e6b', + 'expected_class' => 'wp-container-core-group-is-layout-5a23bf8e', ), 'grid type 3 columns' => array( 'block_attributes' => array( @@ -633,7 +643,7 @@ public function data_layout_support_flag_renders_consistent_container_hash() { 'columnCount' => 3, ), ), - 'expected_class' => 'wp-container-core-group-is-layout-d3b710ac', + 'expected_class' => 'wp-container-core-group-is-layout-cda6dc4f', ), ); } diff --git a/tests/phpunit/tests/kses/wpKsesHair.php b/tests/phpunit/tests/kses/wpKsesHair.php index 05d573bc070bc..0ef834880cd5c 100644 --- a/tests/phpunit/tests/kses/wpKsesHair.php +++ b/tests/phpunit/tests/kses/wpKsesHair.php @@ -39,6 +39,28 @@ public function test_attribute_parsing( string $input, array $expected ) { * @return Generator */ public function data_attribute_parsing() { + yield 'empty attributes' => array( + '', + array(), + ); + + yield 'prematurely-terminated attributes' => array( + '>', + array(), + ); + + yield 'prematurely-terminated malformed attributes' => array( + 'foo>bar="baz"', + array( + 'foo' => array( + 'name' => 'foo', + 'value' => '', + 'whole' => 'foo', + 'vless' => 'y', + ), + ), + ); + yield 'single attribute with double quotes' => array( 'class="test-class"', array( diff --git a/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php b/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php index b1f6203e6bb00..b510edcbfd0cb 100644 --- a/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php +++ b/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php @@ -290,4 +290,32 @@ public function test_should_remove_multiple_declarations() { 'Output after removing "color" and "margin" declarations via `remove_declarations()` does not match expectations' ); } + + /** + * Tests that non-string values are rejected without causing fatal errors. + * + * @ticket 64545 + * + * @covers ::add_declaration + */ + public function test_should_reject_non_string_values() { + $css_declarations = new WP_Style_Engine_CSS_Declarations(); + + // Add valid string value first. + $css_declarations->add_declaration( 'color', 'red' ); + + // Try to add array value - should be silently rejected. + $css_declarations->add_declaration( 'padding-margin', array( 'top' => '10px' ) ); + + // Try to add other non-string values. + $css_declarations->add_declaration( 'font-size', 123 ); + $css_declarations->add_declaration( 'margin', null ); + + // Only the valid string value should be stored. + $this->assertSame( + array( 'color' => 'red' ), + $css_declarations->get_declarations(), + 'Non-string values should be rejected without causing errors.' + ); + } } diff --git a/tools/gutenberg/copy-gutenberg-build.js b/tools/gutenberg/copy-gutenberg-build.js index a66ca113e0cc2..aa30d92264bf9 100644 --- a/tools/gutenberg/copy-gutenberg-build.js +++ b/tools/gutenberg/copy-gutenberg-build.js @@ -91,22 +91,6 @@ const COPY_CONFIG = { ], }, - // PHP source files (non-block files, copied from packages) - phpSource: { - files: [ - { - // Block parser classes - package: 'block-serialization-default-parser', - files: [ - 'class-wp-block-parser.php', - 'class-wp-block-parser-block.php', - 'class-wp-block-parser-frame.php', - ], - destination: '', // Root of wp-includes - }, - ], - }, - // Theme JSON files (from Gutenberg lib directory) themeJson: { files: [ @@ -1028,38 +1012,7 @@ async function main() { ); copyBlockAssets( COPY_CONFIG.blocks ); - // 6. Copy non-block PHP source files (from packages) - console.log( '\n📦 Copying non-block PHP files...' ); - const phpSourceConfig = COPY_CONFIG.phpSource; - - for ( const fileGroup of phpSourceConfig.files ) { - const packageSrc = path.join( gutenbergPackagesDir, fileGroup.package ); - - if ( ! fs.existsSync( packageSrc ) ) { - console.log( ` ⚠️ Package not found: ${ fileGroup.package }` ); - continue; - } - - for ( const file of fileGroup.files ) { - const src = path.join( packageSrc, file ); - const dest = path.join( - wpIncludesDir, - fileGroup.destination, - file - ); - - if ( fs.existsSync( src ) ) { - fs.mkdirSync( path.dirname( dest ), { recursive: true } ); - let content = fs.readFileSync( src, 'utf8' ); - fs.writeFileSync( dest, content ); - } - } - console.log( - ` ✅ ${ fileGroup.package } (${ fileGroup.files.length } files)` - ); - } - - // 7. Copy theme JSON files (from Gutenberg lib directory) + // 6. Copy theme JSON files (from Gutenberg lib directory) console.log( '\n📦 Copying theme JSON files...' ); const themeJsonConfig = COPY_CONFIG.themeJson; const gutenbergLibDir = path.join( gutenbergDir, 'lib' ); @@ -1086,19 +1039,19 @@ async function main() { } } - // 9. Generate script-modules-packages.min.php from individual asset files + // 7. Generate script-modules-packages.min.php from individual asset files console.log( '\n📦 Generating script-modules-packages.min.php...' ); generateScriptModulesPackages(); - // 10. Generate script-loader-packages.min.php + // 8. Generate script-loader-packages.min.php console.log( '\n📦 Generating script-loader-packages.min.php...' ); generateScriptLoaderPackages(); - // 11. Generate require-dynamic-blocks.php and require-static-blocks.php + // 9. Generate require-dynamic-blocks.php and require-static-blocks.php console.log( '\n📦 Generating block registration files...' ); generateBlockRegistrationFiles(); - // 12. Generate blocks-json.php from block.json files + // 10. Generate blocks-json.php from block.json files console.log( '\n📦 Generating blocks-json.php...' ); generateBlocksJson();