Thanks for your reply, @timkaye!
At the moment I wrote those bem-specific functions (~2 years ago), there were no direct filters for classnames. At least, I could not find them, reading not only documentation, but the original “nav-menu-template.php” file too.
Just checked Codex. There are still only 3 related hook filters (others are for the different purposes).
Two of them contain already generated HTML (for seprate items and menu in whole):
wp_nav_menu_items / wp_nav_menu_{$menu->slug}_items
Filter Hook: Filters the HTML list content for navigation menus.
wp_nav_menu
Filters the HTML content for navigation menus.
…but parsing HTML in this case is a hell-alike, because classnames vary a lot and there are tons of combinations to search and replace. I use ‘wp_nav_menu’ + preg_replace to rename “.sub-menu” & “.sub-sub-menu” as those container classes don’t belong to ‘wp_nav_menu_objects’ and there is no other way to change them.
The third hook is exactly what I use in my current workaround:
wp_nav_menu_objects
Filter Hook: Filters the sorted list of menu item objects before generating the menu’s HTML.
It passes an array of menu items (as objects). Each item has a property $item->classes, containing an array of applied classnames. So, as I mentioned above, it’s a rather ugly workaround. Looping N items * M classnames and comparing each to a predefined list of possible values is not the best solution ever.
// Filtering Nav Menu objects for
function bem_nav_menu_objects( $items, $menu_args ) {
if ( ! empty( $items ) ) {
foreach ( $items as $item ) {
if ( ! empty( $item->classes ) ) {
$item->classes = bem_nav_menu_item_classes(
$item->classes,
$menu_args->menu
);
}
}
}
return $items;
}
add_filter( 'wp_nav_menu_objects', 'bem_nav_menu_objects' ,10, 2 );
// Replacing standard classnames in nav menu items
function bem_nav_menu_item_classes( $classes = array(), $menu_name = 'menu' ) {
$default_class = 'menu-item';
// Default BEM block settings
$bem_block = $menu_name;
$bem_element = 'item';
$bem_mods = array();
if ( ! empty( $classes ) ) {
foreach ( $classes as $class ) {
$mod_str = '';
// Trying to split classname by standard prefix 'menu-item-'
$temp = explode($default_class . '-', $class, 2);
if ( ! empty( $temp[1] ) ) {
// It may contain "has-children"
if ( $temp[1] === 'has-children') {
// Adding this flag as a boolean modifier
$mod_str .= $temp[1];
} else {
// Other class names are key-value pairs, trying to split
$m = explode( '-', $temp[1], 2 );
if ( ! empty( $m[0] ) ) {
// Using the key as modifier name
$mod_str .= $m[0];
if ( ! empty( $m[1] ) ) {
// Formatting value to BEM notation (underslashes turn into hyphens)
$mod_str .= '_' .
str_replace( '_', '-', $m[1] );
}
}
}
} else {
// If it's not a 'menu-item-...' class, assume it is a 'current_page_...'
$temp = explode('current_page_', $class, 2);
if ( ! empty( $temp[1] ) ) {
// Formatting it as a value for a 'relation' modifier
//$mod_str = 'relation_current-page-' . $temp[1];
} else {
// It might also be a 'current-...' classname
$temp = explode('current-', $class, 2);
if ( ! empty( $temp[1] ) ) {
// Current item should be marked by 'active' modifier
if ($class === 'current-menu-item') {
$bem_mods[] = 'active';
}
// It might be an ancestor or a descender, writing it to 'relation' modifier
//$mod_str = 'relation_current-' . $temp[1];
} else {
// Maybe its a page? Checking for 'page-item-'
$temp = explode('page-item-', $class, 2);
if ( ! empty( $temp[1] ) ) {
// It's a 'relation', too
//$mod_str = 'relation_page-item-' .
$temp[1];
}
// page_item class itself is a 'relation', once again
elseif ( $class === 'page_item') {
//$mod_str = 'relation_page-item';
} else {
// Custom class? Plugged? Unknown? Let it be...
$mod_str = $class;
}
}
}
}
// Adding a formed modifier to common array of modifiers
$bem_mods[] = $mod_str;
}
// Removing possible duplicates
$bem_mods = array_unique( $bem_mods );
}
// Generating proper BEM class with all that mofiers
return bem_get_classes( $bem_block, $bem_element, $bem_mods );
}
// Replacing .sub-menu & .sub-sub-menu classes for conatiners (ul's)
function bem_nav_menu_filter( $nav_menu, $args ) {
return preg_replace(
'/((sub-)+)(menu)/i',
$args->menu . '__$1$3',
$nav_menu
);
}
add_filter( 'wp_nav_menu', 'bem_nav_menu_filter', 9, 2);
// Replacing .sublink & .link in links attributes
function bem_nav_menu_link_attributes ( $atts, $item, $args, $depth ) {
if ( $depth > 0) {
$atts['class'] = bem_get_class( $args->menu, 'sublink' );
} else {
$atts['class'] = bem_get_class( $args->menu, 'link');
}
return $atts;
}
add_filter( 'nav_menu_link_attributes',
'bem_nav_menu_link_attributes', 9, 4 );
...
And all that spaghetti is a result of hardcoded classnames. I’m not a skilled developer (not a developer at all, trully saying), so I can’t write a beautiful regexp which magically solves all my problems And refactoring that nightmare hangs in my todo since 2017. Well, maybe next year