Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions packages/block-library/src/navigation-link/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,8 @@
* @package WordPress
*/

// Path differs between source and build: './shared/' in source, './navigation-link/shared/' in build.
if ( file_exists( __DIR__ . '/shared/item-should-render.php' ) ) {
require_once __DIR__ . '/shared/item-should-render.php';
require_once __DIR__ . '/shared/render-submenu-icon.php';
} else {
require_once __DIR__ . '/navigation-link/shared/item-should-render.php';
require_once __DIR__ . '/navigation-link/shared/render-submenu-icon.php';
}
require_once __DIR__ . '/navigation-link/shared/item-should-render.php';
require_once __DIR__ . '/navigation-link/shared/render-submenu-icon.php';

/**
* Build an array with CSS classes and inline styles defining the colors
Expand Down Expand Up @@ -170,7 +164,7 @@ function block_core_navigation_link_maybe_urldecode( $url ) {
function render_block_core_navigation_link( $attributes, $content, $block ) {
// Check if this navigation item should render based on post status.
if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
if ( ! gutenberg_block_core_shared_navigation_item_should_render( $attributes, $block ) ) {
if ( ! block_core_shared_navigation_item_should_render( $attributes, $block ) ) {
return '';
}
}
Expand Down Expand Up @@ -262,7 +256,7 @@ function render_block_core_navigation_link( $attributes, $content, $block ) {

if ( isset( $block->context['showSubmenuIcon'] ) && $block->context['showSubmenuIcon'] && $has_submenu ) {
// The submenu icon can be hidden by a CSS rule on the Navigation Block.
$html .= '<span class="wp-block-navigation__submenu-icon">' . block_core_navigation_render_submenu_icon() . '</span>';
$html .= '<span class="wp-block-navigation__submenu-icon">' . block_core_shared_navigation_render_submenu_icon() . '</span>';
}

if ( $has_submenu ) {
Expand Down
16 changes: 5 additions & 11 deletions packages/block-library/src/navigation-submenu/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,8 @@ function block_core_navigation_submenu_get_submenu_visibility( $context ) {
return $submenu_visibility ?? 'hover';
}

// Path differs between source and build: '../navigation-link/shared/' in source, './navigation-link/shared/' in build.
if ( file_exists( __DIR__ . '/../navigation-link/shared/item-should-render.php' ) ) {
require_once __DIR__ . '/../navigation-link/shared/item-should-render.php';
require_once __DIR__ . '/../navigation-link/shared/render-submenu-icon.php';
} else {
require_once __DIR__ . '/navigation-link/shared/item-should-render.php';
require_once __DIR__ . '/navigation-link/shared/render-submenu-icon.php';
}
require_once __DIR__ . '/navigation-link/shared/item-should-render.php';
require_once __DIR__ . '/navigation-link/shared/render-submenu-icon.php';

/**
* Build an array with CSS classes and inline styles defining the font sizes
Expand Down Expand Up @@ -106,7 +100,7 @@ function block_core_navigation_submenu_build_css_font_sizes( $context ) {
function render_block_core_navigation_submenu( $attributes, $content, $block ) {
// Check if this navigation item should render based on post status.
if ( defined( 'IS_GUTENBERG_PLUGIN' ) && IS_GUTENBERG_PLUGIN ) {
if ( ! gutenberg_block_core_shared_navigation_item_should_render( $attributes, $block ) ) {
if ( ! block_core_shared_navigation_item_should_render( $attributes, $block ) ) {
return '';
}
}
Expand Down Expand Up @@ -240,7 +234,7 @@ function render_block_core_navigation_submenu( $attributes, $content, $block ) {
if ( $show_submenu_indicators && $has_submenu ) {
// The submenu icon is rendered in a button here
// so that there's a clickable element to open the submenu.
$html .= '<button aria-label="' . esc_attr( $aria_label ) . '" class="wp-block-navigation__submenu-icon wp-block-navigation-submenu__toggle" aria-expanded="false">' . block_core_navigation_render_submenu_icon() . '</button>';
$html .= '<button aria-label="' . esc_attr( $aria_label ) . '" class="wp-block-navigation__submenu-icon wp-block-navigation-submenu__toggle" aria-expanded="false">' . block_core_shared_navigation_render_submenu_icon() . '</button>';
}
} else {
$html .= '<button aria-label="' . esc_attr( $aria_label ) . '" class="wp-block-navigation-item__content wp-block-navigation-submenu__toggle" aria-expanded="false">';
Expand All @@ -262,7 +256,7 @@ function render_block_core_navigation_submenu( $attributes, $content, $block ) {
$html .= '</button>';

if ( $has_submenu ) {
$html .= '<span class="wp-block-navigation__submenu-icon">' . block_core_navigation_render_submenu_icon() . '</span>';
$html .= '<span class="wp-block-navigation__submenu-icon">' . block_core_shared_navigation_render_submenu_icon() . '</span>';
}
}

Expand Down
11 changes: 0 additions & 11 deletions packages/block-library/src/navigation/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -1301,17 +1301,6 @@ function block_core_navigation_build_css_font_sizes( $attributes ) {
return $font_sizes;
}

/**
* Returns the top-level submenu SVG chevron icon.
*
* @since 5.9.0
*
* @return string
*/
function block_core_navigation_render_submenu_icon() {
return '<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 12 12" fill="none" aria-hidden="true" focusable="false"><path d="M1.50002 4L6.00002 8L10.5 4" stroke-width="1.5"></path></svg>';
}

/**
* Filter out empty "null" blocks from the block list.
* 'parse_blocks' includes a null block with '\n\n' as the content when
Expand Down
66 changes: 59 additions & 7 deletions packages/wp-build/lib/build.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,7 @@ function normalizePath( p ) {
return p.replace( /\\/g, '/' );
}

function transformPhpContent( content, transforms ) {
function transformPhpContent( content, transforms, allFunctionNames = [] ) {
const {
functionPrefix = '',
classSuffix = '',
Expand All @@ -328,14 +328,42 @@ function transformPhpContent( content, transforms ) {
}

if ( functionPrefix ) {
content = Array.from(
content.matchAll( /^\s*function ([^\(]+)/gm )
).reduce( ( result, [ , functionName ] ) => {
return result.replace(
// Find functions defined in this file.
const localFunctions = new Set(
Array.from( content.matchAll( /^\s*function ([^\(]+)/gm ) ).map(
( [ , name ] ) => name
)
);

// Rename locally-defined functions and all their references in this file.
for ( const functionName of localFunctions ) {
// Skip functions already prefixed (e.g., by the prefixFunctions step above).
if ( functionName.startsWith( functionPrefix ) ) {
continue;
}
content = content.replace(
new RegExp( functionName + '(?![a-zA-Z0-9_])', 'g' ),
Comment on lines +344 to 345

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local function renaming regex only enforces an identifier boundary at the end of the match. Without a start boundary it can rename substrings inside longer identifiers (e.g. my_block_core_foo containing block_core_foo). Align this with the cross-file regex by enforcing a start boundary (and escaping the function name when building the RegExp).

Suggested change
content = content.replace(
new RegExp( functionName + '(?![a-zA-Z0-9_])', 'g' ),
const escapedFunctionName = functionName.replace(
/[.*+?^${}()|[\]\\]/g,
'\\$&'
);
const localFuncPattern =
'(?<![a-zA-Z0-9_])' + escapedFunctionName + '(?![a-zA-Z0-9_])';
content = content.replace(
new RegExp( localFuncPattern, 'g' ),

Copilot uses AI. Check for mistakes.
( match ) => functionPrefix + match.replace( /^wp_/, '' )
);
}, content );
}

// Prefix cross-file function references (functions defined in other
// files within the same package). Skip functions that are local to
// this file since they were already handled above.
const crossFileFunctions = allFunctionNames.filter(
( name ) => ! localFunctions.has( name )
);
if ( crossFileFunctions.length ) {
const funcPattern = crossFileFunctions.join( '|' );
content = content.replace(
new RegExp(
'(?<![a-zA-Z0-9_])(?:' + funcPattern + ')(?![a-zA-Z0-9_])',
'g'
),
( match ) =>
`${ functionPrefix }${ match.replace( /^wp_/, '' ) }`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this use the same replace regex/method as one of the ones above?

);
Comment on lines +357 to +365

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cross-file prefixing content.replace(...) runs over the entire PHP file, so it will also rewrite occurrences inside PHP comments. That can unintentionally change documentation (e.g. comments that mention another file’s helper function) and contradicts the stated goal of leaving comments untouched. Consider masking/removing comment tokens during the replacement so only code and string literals are processed.

Copilot uses AI. Check for mistakes.
Comment on lines +350 to +365

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

allFunctionNames may include functions that already start with the functionPrefix (e.g. a source file defining gutenberg_*). In that case the cross-file prefixing step will match them and prepend functionPrefix again, producing double-prefixed calls. Filter crossFileFunctions to exclude names that already start with the prefix (and/or make the replace callback a no-op when match is already prefixed).

Copilot uses AI. Check for mistakes.
}
}

if ( addActionPriority ) {
Expand Down Expand Up @@ -671,6 +699,29 @@ async function bundlePackage( packageName, options = {} ) {
const packageSourceDir = path.join( packageDir, 'src' );
const outputDir = path.join( BUILD_DIR, 'scripts', packageName );

// Collect all function definitions across PHP files so that
// cross-file references can be auto-prefixed without needing
// a manual prefixFunctions entry.
const allFunctionNames = [];
if ( transforms.php?.functionPrefix ) {
for ( const filePattern of files ) {
const matchedFiles = await glob(
normalizePath( path.join( packageDir, filePattern ) )
);
for ( const sourceFile of matchedFiles ) {
if ( ! sourceFile.endsWith( '.php' ) ) {
continue;
}
const content = await readFile( sourceFile, 'utf8' );
for ( const [ , name ] of content.matchAll(
/^\s*function ([^\(]+)/gm
) ) {
allFunctionNames.push( name );
}
}
}
Comment on lines +702 to +722

Copilot AI Mar 2, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This adds an extra full read/scan of all PHP source files to collect allFunctionNames, and then the build loop reads each file again for transformation. For large packages this is a noticeable build-time regression. Consider collecting definitions while doing the existing per-file reads (e.g. first pass cache of file contents, or a single pass that both collects names and writes outputs) and de-duplicating the function-name list to keep the generated regex smaller.

Copilot uses AI. Check for mistakes.
}

for ( const filePattern of files ) {
const matchedFiles = await glob(
normalizePath( path.join( packageDir, filePattern ) )
Expand All @@ -696,7 +747,8 @@ async function bundlePackage( packageName, options = {} ) {
);
const transformed = transformPhpContent(
content,
transforms.php
transforms.php,
allFunctionNames
);

if ( transforms.php.filenameSuffix ) {
Expand Down
Loading