$atts Shortcode attributes. * @return string HTML output. */ public function render( array $atts = [] ): string { $atts = shortcode_atts( [ 'view' => 'month', 'category' => '', 'user' => '', 'users' => '', 'show_categories' => 'true', 'show_filters' => 'true', 'initial_month' => '', 'list_start' => 'month', 'list_mode' => 'pages', 'list_height' => '', ], $atts, 'webcalendar' ); // Enqueue frontend assets wp_enqueue_script( 'webcal-frontend' ); wp_enqueue_style( 'webcal-frontend' ); $initial_month = preg_match( '/^\d{4}-\d{2}$/', $atts['initial_month'] ) ? $atts['initial_month'] : ''; // Where List view begins: the 1st of the month (default) or today. $list_start = 'today' === $atts['list_start'] ? 'today' : 'month'; // Continuous: List view keeps loading upcoming events as you scroll. $list_mode = 'continuous' === $atts['list_mode'] ? 'continuous' : 'pages'; $list_height = self::list_height( $atts['list_height'] ); [ $category, $unknown_categories ] = $this->resolve_categories( $atts['category'] ); // Pass attributes to JS wp_localize_script( 'webcal-frontend', 'webcalData', [ 'view' => $atts['view'], 'category' => $category, 'user' => $atts['user'], 'users' => sanitize_text_field( $atts['users'] ), 'showCategories' => $atts['show_categories'] !== 'false', 'showFilters' => $atts['show_filters'] !== 'false', // Venue and organizer filtering is Starter+. The server ignores // those parameters without a licence, so this only decides // whether the controls are worth rendering at all. 'canFilterByPlace' => \webcal_feature_available( 'filter_by_place' ), 'initialMonth' => $initial_month, 'listStart' => $list_start, 'listMode' => $list_mode, 'listHeight' => $list_height, 'nonce' => wp_create_nonce( 'wp_rest' ), 'apiUrl' => get_rest_url( null, 'webcal/v1' ), ] ); // Every substituted value is esc_attr()'d; wp_kses() is a defensive // boundary on the returned markup so the shortcode contract is // verifiably safe even if a future edit forgets an inner escape (WPORG-13.2). $notice = ''; if ( [] !== $unknown_categories && current_user_can( 'edit_posts' ) ) { $notice = '
' . esc_html( sprintf( /* translators: %s: comma-separated category names from the shortcode. */ __( 'WebCalendar: no category named %s. Only editors see this message.', 'agenticdaisy-calendar' ), implode( ', ', $unknown_categories ) ) ) . '
'; } return wp_kses( $notice . sprintf( '', esc_attr( $atts['view'] ), esc_attr( $category ), esc_attr( $initial_month ), esc_attr( sanitize_text_field( $atts['users'] ) ), esc_attr( $atts['show_categories'] !== 'false' ? 'true' : 'false' ), esc_attr( $atts['show_filters'] !== 'false' ? 'true' : 'false' ), esc_attr( $list_start ), esc_attr( $list_mode ), esc_attr( $list_height ) ), [ 'p' => [ 'class' => [], ], 'div' => [ 'class' => [], 'data-view' => [], 'data-category' => [], 'data-initial-month' => [], 'data-users' => [], 'data-show-categories' => [], 'data-show-filters' => [], 'data-list-start' => [], 'data-list-mode' => [], 'data-list-height' => [], ], ] ); } /** * Turn the category attribute into the id list the REST API takes. * * Each comma-separated entry is a category id or a category name. Names * match the site's global categories, ignoring case and slug form, so * "team-training" finds "Team Training". Tags share the categories table * and are skipped. Personal categories are not considered: a page is the * same for every visitor, and those differ per viewer. * * When entries were given but none resolved, the result is "0", which * the API reads as a filter matching nothing. An empty result would mean * "every category" and turn a typo into the whole calendar. * * @param string $raw The shortcode's category attribute. * @return array{0: string, 1: list