PluginProbe
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) / 2.1.3
Darkify – Dark Mode & Night Mode for Website & Admin (Dark Theme Included) v2.1.3
2.1.3 2.1.2 2.1.1 2.1.0 2.0.4 2.0.3 2.0.2 2.0.1 2.0.0 1.5.5 1.5.4 1.5.3 1.5.2 1.5.1 1.5.0 trunk 1.0.1 1.1.0 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.3.0 All 57 releases
← All changes | src/Frontend/Frontend.php +144 -0 2.0.42.1.3 View file →
@@ -83,8 +83,11 @@
83 83 // PreviewRest::maybe_enable_preview), could never show a newly enabled
84 84 // menu switch until saved. The filter runs at render time, after `init`,
85 85 // so re-reading the option in the callback sees the preview values.
86 86 add_filter('wp_nav_menu_items', array($this, 'darkify_switch_in_menu'), 10, 2);
87 + // Block-theme and Kadence navigations never fire `wp_nav_menu_items` —
88 + // they are blocks, not wp_nav_menu() calls — so they need their own hook.
89 + add_filter('render_block', array($this, 'darkify_switch_in_block_menu'), 10, 2);
87 90
88 91 add_action('wp_enqueue_scripts', array($this, 'darkify_client_enqueue'), 100);
89 92 add_action('login_enqueue_scripts', array($this, 'darkify_client_enqueue'));
90 93 add_action('register_enqueue_scripts', array($this, 'darkify_client_enqueue'));
@@ -511,6 +514,147 @@
511 514 }
512 515 }
513 516
514 517 return $items;
518 + }
519 +
520 + /**
521 + * Put the switch inside a block-theme or Kadence navigation.
522 + *
523 + * `wp_nav_menu_items` only fires for classic wp_nav_menu() output, so on a
524 + * block theme (and on any site using Kadence Navigation) the Menu Switch
525 + * feature had nothing to hook: the menu rendered, the setting was saved, and
526 + * no switch ever appeared. These navigations are blocks, so this is the
527 + * equivalent hook for them.
528 + *
529 + * The selection is matched on the namespaced `<post_type>:<id>` key written
530 + * by the picker (see AbstractRestController::darkify_block_menu_sources()),
531 + * which is what keeps block and Kadence menu POST ids from being confused
532 + * with classic menu TERM ids.
533 + *
534 + * @param string $block_content Rendered HTML for this block.
535 + * @param array $block Parsed block, including its attributes.
536 + * @return string
537 + */
538 + function darkify_switch_in_block_menu($block_content, $block)
539 + {
540 + // render_block fires for every block on the page, so reject as early and
541 + // as cheaply as possible: two string comparisons for the overwhelming
542 + // majority of blocks, and nothing else.
543 + if (empty($block['blockName'])) {
544 + return $block_content;
545 + }
546 +
547 + if ($block['blockName'] === 'core/navigation') {
548 + $source = 'wp_navigation';
549 + $ref = isset($block['attrs']['ref']) ? (int) $block['attrs']['ref'] : 0;
550 + } elseif ($block['blockName'] === 'kadence/navigation') {
551 + $source = 'kadence_navigation';
552 + $ref = isset($block['attrs']['id']) ? (int) $block['attrs']['id'] : 0;
553 + } else {
554 + return $block_content;
555 + }
556 +
557 + // A navigation block with no saved menu behind it (unsaved, or rendering
558 + // its fallback) has nothing this setting could have been pointed at.
559 + if ($ref <= 0) {
560 + return $block_content;
561 + }
562 +
563 + $options = get_option('darkify');
564 + $show_in_menu = isset($options['show_in_menu']) ? $options['show_in_menu'] : '';
565 +
566 + // Gated here rather than at filter registration for the same reason as
567 + // the classic path: the live preview toggles this without saving.
568 + if (empty($show_in_menu['enable_switch_in_menu'])) {
569 + return $block_content;
570 + }
571 +
572 + if (! $this->darkify_is_dark_mode_allowed()) {
573 + return $block_content;
574 + }
575 +
576 + $select_menus = isset($show_in_menu['select_menus']) ? $show_in_menu['select_menus'] : [];
577 + $needle = $source . ':' . $ref;
578 + $shortcode = '';
579 + $position = 'after';
580 +
581 + foreach ((array) $select_menus as $select_menu) {
582 + if (($select_menu['switch_in_menu_location'] ?? '') !== $needle) {
583 + continue;
584 + }
585 + // Last match wins, matching the classic path's behaviour. Unlike
586 + // that path the position is read from the row that actually
587 + // matched, so it cannot be taken from an unrelated row.
588 + $shortcode = $select_menu['darkify_menu_shortcode_helper'] ?? '';
589 + $position = $select_menu['select_menu_position'] ?? 'after';
590 + }
591 +
592 + if (! $shortcode) {
593 + return $block_content;
594 + }
595 +
596 + $item = '<li class="menu-item">' . do_shortcode($shortcode) . '</li>';
597 +
598 + $injected = $this->darkify_inject_menu_item($block_content, $item, $position);
599 +
600 + // Unrecognised markup is left exactly as the block rendered it: a
601 + // navigation with no switch is a far better outcome than a broken one.
602 + return ($injected === null) ? $block_content : $injected;
603 + }
604 +
605 + /**
606 + * A `<ul>` or `</ul>` tag, with capture 1 set to "/" on a closing tag.
607 + *
608 + * The alternation skips over quoted attribute values rather than using a
609 + * plain `[^>]*`: an attribute may legitimately contain a `>` (`data-x="a>b"`),
610 + * and a bare `[^>]*` ends the match in the middle of that value. The open
611 + * tag would then appear to end mid-attribute, so a "before" insertion landed
612 + * inside the attribute and broke the markup, and a `>` inside a submenu's
613 + * own `<ul>` attributes miscounted the nesting depth below.
614 + */
615 + const DARKIFY_UL_TAG = '/<(\/?)ul\b(?:[^>"\']|"[^"]*"|\'[^\']*\')*>/i';
616 +
617 + /**
618 + * Insert a list item into the first top-level `<ul>` of a rendered block.
619 + *
620 + * The closing tag is found by balancing `<ul>`/`</ul>` rather than by
621 + * searching for the last `</ul>`: navigations nest submenus, so the last one
622 + * closes a submenu and the switch would land inside a dropdown.
623 + *
624 + * @return string|null Null when the markup has no usable list.
625 + */
626 + private function darkify_inject_menu_item($html, $item, $position)
627 + {
628 + if (! preg_match(self::DARKIFY_UL_TAG, $html, $open, PREG_OFFSET_CAPTURE)) {
629 + return null;
630 + }
631 +
632 + // A fragment that opens on a closing tag has no list to add to.
633 + if ($open[1][0] === '/') {
634 + return null;
635 + }
636 +
637 + $after_open = $open[0][1] + strlen($open[0][0]);
638 +
639 + if ($position === 'before') {
640 + return substr($html, 0, $after_open) . $item . substr($html, $after_open);
641 + }
642 +
643 + $depth = 1;
644 + $offset = $after_open;
645 + $tag = null;
646 +
647 + while ($depth > 0 && preg_match(self::DARKIFY_UL_TAG, $html, $tag, PREG_OFFSET_CAPTURE, $offset)) {
648 + $depth += ($tag[1][0] === '/') ? -1 : 1;
649 + $offset = $tag[0][1] + strlen($tag[0][0]);
650 + }
651 +
652 + if ($depth !== 0 || $tag === null) {
653 + return null;
654 + }
655 +
656 + $before_close = $offset - strlen($tag[0][0]);
657 +
658 + return substr($html, 0, $before_close) . $item . substr($html, $before_close);
515 659 }
516 660 }