PluginProbe
Cooked – Recipe Management / 1.16.1
Cooked – Recipe Management v1.16.1
1.16.1 1.16.0 1.15.0 trunk 1.10.0 1.11.0 1.11.1 1.11.2 1.11.3 1.11.4 1.12.0 1.13.0 1.14.0 1.7.10 1.7.11 1.7.12 1.7.13 1.7.15.1 1.7.15.3 1.7.15.4 1.8.0 1.8.1 1.8.2 1.8.3 1.8.4 All 37 releases
cooked / includes / class.cooked-shortcodes.php

class.cooked-shortcodes.php in Cooked – Recipe Management 1.16.1, at includes/class.cooked-shortcodes.php

1,277 lines 59.3 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Cooked Shortcodes
4 *
5 * @package Cooked
6 * @subpackage Shortcodes
7 * @since 1.0.0
8 */
9
10 // Exit if accessed directly
11 if ( !defined( 'ABSPATH' ) ) exit;
12
13 /**
14 * Cooked_Shortcodes Class
15 *
16 * This class handles the settings creation and contains functions for retreiving those settings.
17 *
18 * @since 1.0.0
19 */
20
21 class Cooked_Shortcodes {
22
23 /**
24 * Recipe IDs currently being rendered by [cooked-recipe] in this request.
25 *
26 * @var int[]
27 */
28 private static $recipe_embed_stack = [];
29
30 function __construct() {
31 // Allow shortcodes in widgets
32 add_filter( 'widget_text', 'do_shortcode' );
33
34 // Site-Wide
35 add_shortcode('cooked-browse', [$this, 'cooked_browse_shortcode'] );
36 add_shortcode('cooked-search', [$this, 'cooked_search_shortcode'] );
37 add_shortcode('cooked-recipe', [$this, 'cooked_recipe_shortcode'] );
38 add_shortcode('cooked-categories', [$this, 'cooked_categories_shortcode'] );
39 add_shortcode('cooked-recipe-list', [$this, 'cooked_recipe_list_shortcode'] );
40 add_shortcode('cooked-recipe-card', [$this, 'cooked_recipe_card_shortcode'] );
41 add_shortcode('cooked-related-recipes', [$this, 'cooked_related_recipes_shortcode'] );
42
43 if ( shortcode_exists( 'timer' ) ):
44 add_shortcode('cooked-timer', [$this, 'cooked_timer'] );
45 else:
46 add_shortcode('cooked-timer', [$this, 'cooked_timer'] );
47 add_shortcode('timer', [$this, 'cooked_timer'] );
48 endif;
49
50 // Recipe-Only
51 add_shortcode('cooked-title', [$this, 'cooked_title_shortcode'] );
52 add_shortcode('cooked-gallery', [$this, 'cooked_gallery_shortcode'] );
53 add_shortcode('cooked-image', [$this, 'cooked_image_shortcode'] );
54 add_shortcode('cooked-info', [$this, 'cooked_info_shortcode'] );
55 add_shortcode('cooked-excerpt', [$this, 'cooked_excerpt_shortcode'] );
56 add_shortcode('cooked-notes', [$this, 'cooked_notes_shortcode'] );
57 add_shortcode('cooked-ingredients', [$this, 'cooked_ingredients_shortcode'] );
58 add_shortcode('cooked-directions', [$this, 'cooked_directions_shortcode'] );
59 add_shortcode('cooked-nutrition', [$this, 'cooked_nutrition_shortcode'] );
60
61 // Add preprocessing filter for shortcodes for Elementor, Divi, and other builders compatibility.
62 add_filter('pre_do_shortcode_tag', [$this, 'preprocess_shortcode'], 10, 4);
63 }
64
65 /**
66 * Preprocess Shortcode.
67 *
68 * @since 1.0.0
69 * @param mixed $output The shortcode output.
70 * @param string $tag The shortcode tag.
71 * @param array $attr The shortcode attributes.
72 * @param array $m The regex matches.
73 * @return mixed
74 */
75 public function preprocess_shortcode($output, $tag, $attr, $m) {
76 // Tags to skip
77 $skip_tags = [
78 'cooked-search',
79 'cooked-browse',
80 'cooked-timer',
81 'cooked-recipe',
82 'cooked-recipe-list',
83 'cooked-recipe-card',
84 ];
85
86 // Only process for Cooked shortcodes
87 if (is_front_page() || strpos($tag, 'cooked-') === false || in_array($tag, $skip_tags)) {
88 return $output;
89 }
90
91 global $recipe_settings, $post;
92
93 // If recipe settings are empty, try to get them.
94 if (empty($recipe_settings)) {
95 // Try to get recipe settings from current post.
96 $post_id = isset($post->ID) ? $post->ID : false;
97 if ($post_id && get_post_type( $post_id ) === 'cp_recipe') {
98 $recipe_settings = Cooked_Recipes::get($post_id, true);
99 } else {
100 // We are in the editor but not on a recipe post type. Maybe a single recipe template?
101 // Uses the first recipe found in the database as a sample.
102 $recipe_settings = Cooked_Recipes::get(false, true, false, 1);
103 }
104
105 // If still empty and we have a specific recipe ID in attributes, try to get them.
106 if (empty($recipe_settings) && isset($attr['id'])) {
107 $recipe_settings = Cooked_Recipes::get(intval($attr['id']), true);
108 }
109 }
110
111 return $output;
112 }
113
114 public function cooked_search_shortcode( $atts, $content = null ) {
115 // Shortcode Attributes
116 $options = shortcode_atts(
117 [
118 'compact' => false,
119 'hide_browse' => false,
120 'hide_sorting' => false,
121 'inline_browse' => false,
122 ], $atts
123 );
124
125 return Cooked_Recipes::recipe_search_box( $options );
126 }
127
128 public function cooked_timer( $atts, $content = null ) {
129 global $cooked_timer_identifier;
130
131 // Shortcode Attributes
132 $atts = shortcode_atts(
133 [
134 'seconds' => 0,
135 'minutes' => 0,
136 'length' => 0, // Deprecated, left here for backwards compatibility with 2.x
137 'hours' => 0,
138 'desc' => ''
139 ], $atts
140 );
141
142 $desc = esc_attr($atts['desc']);
143 $seconds = floatval( $atts['seconds'] );
144 $minutes_value = floatval( $atts['minutes'] );
145 $minutes = $minutes_value ? $minutes_value * 60 : floatval( $atts['length'] ) * 60;
146 $hours = floatval( $atts['hours'] ) * 60 * 60;
147 $seconds = $seconds + $minutes + $hours;
148
149 if (!$cooked_timer_identifier) {
150 $cooked_timer_identifier = 1;
151 } else {
152 $cooked_timer_identifier++;
153 }
154
155 $timer_id = md5( $seconds . $desc . $content ) . '_' . $cooked_timer_identifier;
156 $desc = $desc ? wp_strip_all_tags( $desc ) : wp_strip_all_tags( $content );
157
158 wp_enqueue_script( 'cooked-timer' );
159
160 return '<span class="cooked-timer"><a aria-label="' . esc_attr( $desc ) . '" data-timer-id="' . esc_attr( $timer_id ) . '" data-seconds="' . esc_attr( $seconds ) . '" data-desc="' . ( $desc ) . '"><i class="cooked-icon cooked-icon-clock"></i> ' . wp_kses_post( $content ) . '</a></span>';
161 }
162
163 public function cooked_browse_shortcode( $sc_atts, $content = null ) {
164 global $_cooked_settings;
165
166 if ( isset($_cooked_settings['advanced']) && !empty($_cooked_settings['advanced']) && in_array( 'disable_public_recipes', $_cooked_settings['advanced'] ) ) {
167 /* translators: referring to the bottom of the Settings page. */
168 return current_user_can( 'edit_cooked_settings' ) ? wpautop( sprintf( __('Public recipes are currently disabled. You can change this at the bottom of the %s page.','cooked'), '<a href="' . trailingslashit( esc_url( admin_url() ) ) . 'admin.php?page=cooked_settings" target="_blank">' . __( 'Settings', 'cooked' ) . '</a>' ) ) : false;
169 }
170
171 if ( is_admin() ) return false;
172
173 $author_query_var = sanitize_key( get_query_var( 'recipe_author', false ) );
174 // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Display-only browse author filter.
175 if ( ! $author_query_var && isset( $_GET['recipe_author'] ) ) {
176 $author_query_var = sanitize_key( wp_unslash( $_GET['recipe_author'] ) );
177 }
178 // phpcs:enable WordPress.Security.NonceVerification.Recommended
179
180 // Shortcode Attributes
181 $atts = shortcode_atts( apply_filters( 'cooked_browse_shortcode_default_attributes', [
182 'category' => false,
183 'order' => false,
184 'orderby' => false,
185 'show' => false,
186 'search' => 'true',
187 'pagination' => 'true',
188 'columns' => 3,
189 'layout' => false,
190 'author' => $author_query_var,
191 'compact' => false,
192 'hide_browse' => false,
193 'hide_sorting' => false,
194 'exclude' => false,
195 'inline_browse' => false,
196 'hide_excerpt' => false,
197 ] ), $sc_atts);
198
199 return Cooked_Recipes::list_view( $atts );
200 }
201
202 public function cooked_recipe_card_shortcode( $atts, $content = null ) {
203 if ( is_admin() ) return false;
204
205 // Shortcode Attributes
206 $atts = shortcode_atts([
207 'id' => false,
208 'category' => false,
209 'width' => false,
210 'style' => false,
211 'hide_total' => false,
212 'hide_image' => false,
213 'hide_title' => false,
214 'hide_excerpt' => false,
215 'hide_author' => false
216 ], $atts);
217
218 $recipe_id = intval( $atts['id'] );
219 $category_id = intval( $atts['category'] );
220 $width = Cooked_Functions::sanitize_text_field( $atts['width'] );
221 $valid_styles = [ 'modern', 'modern-centered' ];
222 $style = in_array( $atts['style'], $valid_styles, true ) ? $atts['style'] : false;
223 $hide_image = Cooked_Functions::sanitize_text_field( $atts['hide_image'] );
224 $hide_total = Cooked_Functions::sanitize_text_field( $atts['hide_total'] );
225 $hide_title = Cooked_Functions::sanitize_text_field( $atts['hide_title'] );
226 $hide_excerpt = Cooked_Functions::sanitize_text_field( $atts['hide_excerpt'] );
227 $hide_author = Cooked_Functions::sanitize_text_field( $atts['hide_author'] );
228
229 ob_start();
230
231 if ( $recipe_id ) {
232 echo wp_kses_post( Cooked_Recipes::card( $recipe_id, $width, $hide_image, $hide_title, $hide_excerpt, $hide_author, $style ) );
233 } elseif ( $category_id ) {
234 echo wp_kses_post( Cooked_Taxonomies::card( $category_id, $width, $hide_image, $hide_total, $style ) );
235 }
236
237 return ob_get_clean();
238 }
239
240 public function cooked_categories_shortcode( $atts, $content = null ) {
241 if ( is_admin() ) return false;
242
243 // Shortcode Attributes
244 $atts = shortcode_atts([
245 'hide_empty' => true,
246 'child_of' => false,
247 'style' => 'block'
248 ], $atts);
249
250 $hide_empty = Cooked_Functions::sanitize_text_field( $atts['hide_empty'] );
251 $child_of = Cooked_Functions::sanitize_text_field( $atts['child_of'] );
252 $valid_styles = [ 'block', 'list' ];
253 $style = in_array( $atts['style'], $valid_styles, true ) ? $atts['style'] : 'block';
254 $parents_only = $child_of ? false : true;
255
256 ob_start();
257
258 $categories_array = Cooked_Settings::terms_array( 'cp_recipe_category', false, false, $hide_empty, $parents_only, $child_of );
259 if ( !empty($categories_array) ) {
260 echo $style == 'list' ? '<div class="cooked-recipe-term-list">' : '<div class="cooked-recipe-term-grid cooked-clearfix">';
261 foreach ( $categories_array as $key => $val ) {
262 Cooked_Taxonomies::single_taxonomy_block( $key, $style );
263 }
264 echo '</div>';
265 }
266
267 return ob_get_clean();
268 }
269
270 public function cooked_recipe_list_shortcode( $atts, $content = null ) {
271 if ( is_admin() ) return false;
272
273 // Shortcode Attributes
274 $atts = shortcode_atts( [
275 'orderby' => 'date',
276 'width' => '100%',
277 'recipes' => false,
278 'show' => 5,
279 'hide_image' => false,
280 'hide_author' => false
281 ], $atts);
282
283 $recipes = Cooked_Functions::sanitize_text_field( $atts['recipes'] );
284 $orderby = Cooked_Functions::sanitize_text_field( $atts['orderby'] );
285 $show = Cooked_Functions::sanitize_text_field( $atts['show'] );
286 $width = Cooked_Functions::sanitize_text_field( $atts['width'] );
287 $hide_image = Cooked_Functions::sanitize_text_field( $atts['hide_image'] );
288 $hide_author = Cooked_Functions::sanitize_text_field( $atts['hide_author'] );
289
290 if ( $recipes ) {
291 $recipes = preg_replace( '/\s+/', '', $recipes );
292 $recipes = explode( ',', $recipes );
293 }
294
295 ob_start();
296
297 Cooked_Recipes::recipe_list( $orderby, $show, $recipes, $width, $hide_image, $hide_author );
298
299 return ob_get_clean();
300 }
301
302 public function cooked_recipe_shortcode( $atts, $content = null ) {
303 if ( is_admin() ) return false;
304
305 // Shortcode Attributes
306 $atts = shortcode_atts([
307 'id' => false,
308 ], $atts);
309
310 global $recipe_settings, $_cooked_content_unfiltered;
311
312 $recipe_id = intval( $atts['id'] );
313
314 if ( ! $recipe_id ) {
315 return '';
316 }
317
318 if ( in_array( $recipe_id, self::$recipe_embed_stack, true ) ) {
319 $output = self::recipe_embed_blocked_message( $recipe_id );
320
321 if ( $output ) {
322 $output = '<div id="cooked-recipe-embed-' . intval( $recipe_id ) . '" class="cooked-recipe-embed">' . $output . '</div>';
323 $output = apply_filters( 'cooked_recipe_shortcode_output', $output, $recipe_id );
324 }
325
326 return $output;
327 }
328
329 $recipe_settings = Cooked_Recipes::get( $recipe_id, true );
330 if ( ! $recipe_settings || empty( $recipe_settings ) ) {
331 $output = wpautop( '<strong>[cooked-recipe id="' . intval( $recipe_id ) . '"]</strong><br><em>' . __( '(recipe not found or in draft status)', 'cooked' ) . '</em>' );
332
333 if ( $output ) {
334 $output = '<div id="cooked-recipe-embed-' . intval( $recipe_id ) . '" class="cooked-recipe-embed">' . $output . '</div>';
335 $output = apply_filters( 'cooked_recipe_shortcode_output', $output, $recipe_id );
336 }
337
338 return $output;
339 }
340
341 self::$recipe_embed_stack[] = $recipe_id;
342
343 try {
344 ob_start();
345 load_template( COOKED_DIR . 'templates/front/recipe.php', false );
346 $output = do_shortcode( ob_get_clean() );
347
348 if ( $output ) {
349 $output = '<div id="cooked-recipe-embed-' . intval( $recipe_id ) . '" class="cooked-recipe-embed">' . $output . '</div>';
350 $output = apply_filters( 'cooked_recipe_shortcode_output', $output, $recipe_id );
351 }
352
353 return $output;
354 } finally {
355 array_pop( self::$recipe_embed_stack );
356 }
357 }
358
359 private static function recipe_embed_blocked_message( $recipe_id ) {
360 $shortcode = '[cooked-recipe id="' . intval( $recipe_id ) . '"]';
361 $message = '<em>' . sprintf(
362 /* translators: %s: recipe embed shortcode */
363 __( 'This recipe could not be displayed because it is set up to include itself (containing shortcode %s). Remove the duplicate recipe embed from the Recipe Template.', 'cooked' ),
364 $shortcode
365 ) . '</em>';
366
367 $message = apply_filters( 'cooked_recipe_embed_blocked_message', $message, $recipe_id, self::$recipe_embed_stack );
368
369 return wpautop( $message );
370 }
371
372 public function cooked_gallery_shortcode($atts, $content = null) {
373 global $recipe_settings;
374
375 if ( isset($recipe_settings['gallery']) && isset($recipe_settings['gallery']['type']) && $recipe_settings['gallery']['type'] == 'cooked' ) {
376 $gallery_options = apply_filters( 'cooked_recipe_gallery_options', [
377 'data-fit' => 'cover',
378 'data-nav' => 'dots',
379 'data-width' => '100%',
380 'data-loop' => 'true',
381 'data-allowfullscreen' => 'true',
382 'data-ratio' => '800/600',
383 'data-thumbmargin' => '10',
384 'data-thumbborderwidth' => '5',
385 'data-swipe' => 'true',
386 'data-thumbheight' => '75',
387 'data-thumbwidth' => '75'
388 ]);
389
390 // Generate the Shortcode Attributes
391 foreach ( $gallery_options as $opt_name => $opt_value ) {
392 $name_sans_data = str_replace( 'data-', '', $opt_name );
393 $gallery_atts[$name_sans_data] = $opt_value;
394 }
395
396 // Set the Shortcode Attributes
397 $atts = shortcode_atts(
398 $gallery_atts, $atts
399 );
400
401 // Check for Shortcode Attribute Customizations
402 foreach ( $gallery_options as $opt_name => $opt_value ) {
403 $name_sans_data = str_replace( 'data-', '', $opt_name );
404 if ( isset($atts[$name_sans_data]) && $atts[$name_sans_data] ) {
405 $gallery_options[$opt_name] = $atts[$name_sans_data];
406 }
407 }
408 } else {
409 $atts = [];
410 }
411
412 if ( isset($recipe_settings['gallery']) && isset($recipe_settings['gallery']['type']) ):
413
414 switch( $recipe_settings['gallery']['type'] ):
415
416 case 'cooked':
417
418 if ( isset($recipe_settings['gallery']['items']) && !empty($recipe_settings['gallery']['items']) || isset($recipe_settings['gallery']['video_url']) && $recipe_settings['gallery']['video_url'] ):
419
420 // Gallery Options
421 // Developers: Filter these to change!
422 // Full list here: http://fotorama.io/customize/options/
423
424 $gallery_options_array = [];
425
426 foreach ( $gallery_options as $data_key => $data_val ):
427 $gallery_options_array[] = $data_key . '="' . $data_val . '"';
428 endforeach;
429
430 $gallery_html = '<div class="cooked-recipe-gallery"' . ( !empty($gallery_options_array) ? ' ' . implode( ' ', $gallery_options_array ) : '' ) . '>';
431
432 $cooked_gallery_video_last_option = apply_filters( 'cooked_gallery_video_last_option', false );
433
434 if ( !$cooked_gallery_video_last_option && isset($recipe_settings['gallery']['video_url']) && $recipe_settings['gallery']['video_url'] ):
435 $gallery_html .= '<a href="' . esc_url($recipe_settings['gallery']['video_url']) . '" data-caption="' . esc_attr($recipe_settings['title']) . '">' . esc_html($recipe_settings['title']) . '</a>';
436 endif;
437
438 $gallery_items = ( isset($recipe_settings['gallery']['items']) && !empty($recipe_settings['gallery']['items']) ? apply_filters( 'cooked_gallery_items_output', $recipe_settings['gallery']['items'] ) : [] );
439
440 if ( isset($gallery_items) && !empty($gallery_items) ) {
441 foreach ( $gallery_items as $item ) {
442 $image_src = wp_get_attachment_image_src( $item, [900, 900] );
443 $image_title = get_the_title( $item );
444 if ( is_array($image_src) && isset($image_src[0]) ) {
445 $gallery_html .= '<a href="' . esc_url( $image_src[0] ) . '" data-alt="'.esc_attr( $image_title ).'" data-caption="'.esc_attr( $image_title ).'">
446 <img alt="'.esc_attr( $image_title ).'" src="' . wp_get_attachment_image_url( $item, 'thumbnail' ) . '" />
447 </a>';
448 }
449 }
450 }
451
452 if ( $cooked_gallery_video_last_option && isset($recipe_settings['gallery']['video_url']) && $recipe_settings['gallery']['video_url'] ):
453 $gallery_html .= '<a href="' . esc_url($recipe_settings['gallery']['video_url']) . '" data-caption="' . esc_attr($recipe_settings['title']) . '">' . esc_html($recipe_settings['title']) . '</a>';
454 endif;
455
456 // Enqueue Gallery Styles & Scripts
457 wp_enqueue_style( 'cooked-fotorama' );
458 wp_enqueue_script( 'cooked-fotorama' );
459
460 $gallery_html .= '</div>';
461 return wp_kses_post( $gallery_html );
462
463 endif;
464
465 break;
466
467 case 'envira':
468 return ( $recipe_settings['gallery']['envira'] ? '<div class="cooked-recipe-gallery-envira">' . do_shortcode('[envira-gallery id="' . intval( $recipe_settings['gallery']['envira'] ) . '"]') . '</div>' : '' );
469 break;
470
471 case 'soliloquy':
472 return ( $recipe_settings['gallery']['soliloquy'] ? '<div class="cooked-recipe-gallery-soliloquy">' . do_shortcode('[soliloquy id="' . intval( $recipe_settings['gallery']['soliloquy'] ) . '"]') . '</div>' : '' );
473 break;
474
475 case 'revslider':
476 return ( $recipe_settings['gallery']['revslider'] ? '<div class="cooked-recipe-gallery-revslider">' . do_shortcode('[rev_slider alias="' . intval( $recipe_settings['gallery']['revslider'] ) . '"]') . '</div>' : '' );
477 break;
478
479 endswitch;
480
481 endif;
482
483 return false;
484 }
485
486 public function cooked_info_shortcode($atts, $content = null) {
487 global $recipe, $recipe_settings, $_cooked_settings;
488
489 if ( !isset($recipe_settings['id']) && isset($recipe->ID) ):
490 $recipe_settings['id'] = $recipe->ID;
491 elseif ( !isset($recipe_settings['id']) ):
492 global $post;
493 $recipe_settings['id'] = $post->ID;
494 endif;
495
496 // Shortcode Attributes
497 $atts = shortcode_atts([
498 'left' => false,
499 'right' => false,
500 'include' => false,
501 'exclude' => false,
502 ], $atts);
503
504 $left = $atts['left'] ? array_map( 'trim', explode( ',', Cooked_Functions::sanitize_text_field($atts['left']) ) ) : false;
505 $right = $atts['right'] ? array_map( 'trim', explode( ',', Cooked_Functions::sanitize_text_field($atts['right']) ) ) : false;
506 $include = $atts['include'] ? array_map( 'trim', explode( ',', Cooked_Functions::sanitize_text_field($atts['include']) ) ) : false;
507 $exclude = $atts['exclude'] ? array_map( 'trim', explode( ',', Cooked_Functions::sanitize_text_field($atts['exclude']) ) ) : false;
508
509 $default_info_array = apply_filters( 'cooked_default_info_array', [
510 'author' => __('Author', 'cooked'),
511 'difficulty_level' => __('Difficulty', 'cooked'),
512 'servings' => __('Yields', 'cooked'),
513 'prep_time' => __('Prep Time', 'cooked'),
514 'cook_time' => __('Cook Time', 'cooked'),
515 'total_time' => __('Total Time', 'cooked'),
516 'taxonomies' => !empty($_cooked_settings['recipe_taxonomies']) ? $_cooked_settings['recipe_taxonomies'] : []
517 ]);
518
519 if ( $left ):
520 // Left Content
521 foreach( $left as $val ):
522 $info_array['left'][$val] = isset( $default_info_array[$val] ) ? $default_info_array[$val] : '';
523 endforeach;
524 if ( $right ):
525 // Right Content
526 foreach( $right as $val ):
527 $info_array['right'][$val] = isset( $default_info_array[$val] ) ? $default_info_array[$val] : '';
528 endforeach;
529 endif;
530 elseif ( $right ):
531 // Right Content
532 foreach( $right as $val ):
533 $info_array['right'][$val] = isset( $default_info_array[$val] ) ? $default_info_array[$val] : '';
534 endforeach;
535 elseif ( $include ):
536 // Include Content (left)
537 foreach( $include as $val ):
538 $info_array[$val] = isset( $default_info_array[$val] ) ? $default_info_array[$val] : '';
539 endforeach;
540 elseif ( $exclude ):
541 // Exclude Content (left)
542 $info_array = $default_info_array;
543 foreach( $exclude as $val ):
544 unset($info_array[$val]);
545 endforeach;
546 else:
547 $info_array = $default_info_array;
548 endif;
549
550 ob_start();
551
552 if ( !empty( $info_array ) ):
553
554 $available_methods = apply_filters( 'cooked_available_info_shortcode_methods', [
555 'cooked_info_author' => 'Cooked_Recipes',
556 'cooked_info_difficulty' => 'Cooked_Recipes',
557 'cooked_info_servings' => 'Cooked_Recipes',
558 'cooked_info_print' => 'Cooked_Recipes',
559 'cooked_info_fullscreen' => 'Cooked_Recipes',
560 'cooked_info_prep_time' => 'Cooked_Recipes',
561 'cooked_info_cook_time' => 'Cooked_Recipes',
562 'cooked_info_total_time' => 'Cooked_Recipes',
563 'cooked_info_taxonomies' => 'Cooked_Recipes'
564 ]);
565
566 if ( isset($info_array['left']) && !empty($info_array['left']) ):
567 echo '<section class="cooked-left">';
568 foreach( $info_array['left'] as $name => $val ):
569 $function = 'cooked_info_' . $name;
570 if ( array_key_exists( $function, $available_methods ) ):
571 $class = ( $available_methods[$function] == 'Cooked_Recipes' ? $this : $available_methods[$function] );
572 if ( method_exists( $class, $function ) ):
573 $class::$function( $recipe_settings );
574 endif;
575 endif;
576 endforeach;
577 echo '</section>';
578
579 if ( isset($info_array['right']) && !empty($info_array['right']) ):
580 echo '<section class="cooked-right">';
581 foreach( $info_array['right'] as $name => $val ):
582 $function = 'cooked_info_' . $name;
583 if ( array_key_exists( $function, $available_methods ) ):
584 $class = ( $available_methods[$function] == 'Cooked_Recipes' ? $this : $available_methods[$function] );
585 if ( method_exists( $class, $function ) ):
586 $class::$function( $recipe_settings );
587 endif;
588 endif;
589 endforeach;
590 echo '</section>';
591 endif;
592
593 elseif ( isset($info_array['right']) && !empty($info_array['right']) ):
594
595 echo '<section class="cooked-right">';
596 foreach ( $info_array['right'] as $name => $val ):
597 $function = 'cooked_info_' . $name;
598 if ( array_key_exists( $function, $available_methods ) ):
599 $class = ( $available_methods[$function] == 'Cooked_Recipes' ? $this : $available_methods[$function] );
600 if ( method_exists( $class, $function ) ):
601 $class::$function( $recipe_settings );
602 endif;
603 endif;
604 endforeach;
605 echo '</section>';
606
607 else:
608
609 foreach ( $info_array as $name => $val ):
610 $function = 'cooked_info_' . $name;
611 if ( array_key_exists( $function, $available_methods ) ):
612 $class = ( $available_methods[$function] == 'Cooked_Recipes' ? $this : $available_methods[$function] );
613 if ( method_exists( $class, $function ) ):
614 $class::$function( $recipe_settings );
615 endif;
616 endif;
617 endforeach;
618
619 endif;
620
621 endif;
622
623 $cooked_info_html = ob_get_clean();
624
625 if ( $cooked_info_html ):
626 add_filter('wp_kses_allowed_html', [$this, 'cooked_kses_servings_switcher']);
627 add_filter('wp_kses_allowed_html', [$this, 'cooked_kses_cooked_donut']);
628 $cooked_info_html = '<div class="cooked-recipe-info cooked-clearfix">' . wp_kses_post( $cooked_info_html ) . '</div>';
629 $cooked_info_html = apply_filters( 'cooked_info_shortcode_output', $cooked_info_html, $recipe_settings );
630 return $cooked_info_html;
631 //return '<div class="cooked-recipe-info cooked-clearfix">' . $cooked_info_html . '</div>'; // @TODO: Fix this
632 endif;
633 }
634
635 function cooked_kses_servings_switcher($tags) {
636 $tags['select'] = [
637 'name' => true,
638 'id' => true,
639 'class' => true,
640 ];
641 $tags['option'] = [
642 'value' => true,
643 'selected' => true
644 ];
645 return $tags;
646 }
647
648 function cooked_kses_cooked_donut($tags) {
649 $tags['script'] = [
650 'type' => true
651 ];
652 return $tags;
653 }
654
655 public static function cooked_info_author() {
656 global $recipe_settings, $_cooked_settings;
657
658 if (in_array('author', $_cooked_settings['recipe_info_display_options'])) {
659 $browse_page_id = Cooked_Multilingual::get_browse_page_id();
660 $front_page_id = get_option( 'page_on_front' );
661 $browse_page_url = $browse_page_id ? get_permalink( $browse_page_id ) : false;
662 $author = !empty($recipe_settings['author']) ? $recipe_settings['author'] : false;
663
664 if ( !empty($author['id']) && !empty($browse_page_id) ) {
665 // Generate author slug from user_nicename, fallback to user ID if nicename is empty
666 if ( !empty($author['user_nicename']) ) {
667 $author_slug = urlencode(sanitize_title($author['user_nicename']));
668 } else {
669 // Fallback to user ID if user_nicename is empty or missing
670 $author_slug = $author['id'];
671 }
672
673 $permalink = $front_page_id != $browse_page_id && get_option('permalink_structure') ?
674 esc_url( untrailingslashit( $browse_page_url ) . '/' . $_cooked_settings['recipe_author_permalink'] . '/' . trailingslashit( $author_slug ) ) :
675 esc_url( trailingslashit( get_home_url() ) . 'index.php?page_id=' . $browse_page_id . '&recipe_author=' . $author['id'] );
676
677 $permalink = apply_filters( 'cooked_author_permalink', $permalink, $author['id'], $author_slug );
678 } else {
679 $permalink = false;
680 }
681
682 $author_links = isset( $_cooked_settings['disable_author_links'][0] ) && $_cooked_settings['disable_author_links'][0] == 'disabled' ? false : true;
683 $clickable = isset($_cooked_settings['advanced']) && !empty($_cooked_settings['advanced']) && in_array( 'disable_public_recipes', $_cooked_settings['advanced'] ) || !$author_links ? false : true;
684 $hide_avatars = isset( $_cooked_settings['hide_author_avatars'][0] ) && $_cooked_settings['hide_author_avatars'][0] == 'hidden' ? true : false;
685
686 echo '<span class="cooked-author' . ( $hide_avatars ? ' cooked-no-avatar' : '' ) . '">';
687 echo !$hide_avatars ? '<span class="cooked-author-avatar">' . ( !empty($author) ? wp_kses_post( $author['profile_photo'] ) : '' ) . '</span>' : '';
688 echo '<strong class="cooked-meta-title">' . esc_html__('Author', 'cooked') . '</strong>' . ( $clickable && $permalink ? '<a href="' . esc_url( $permalink ) . '">' : '' ) . (!empty($author) ? esc_html( $author['name'] ) : '') . ( $clickable && $permalink ? '</a>' : '' );
689 echo '</span>';
690
691 wp_reset_postdata();
692 }
693 }
694
695 public static function cooked_info_difficulty( $recipe ) {
696 global $_cooked_settings;
697
698 if (in_array('difficulty_level', $_cooked_settings['recipe_info_display_options']) && isset($recipe['difficulty_level']) && $recipe['difficulty_level']) {
699 $dl_html = '<span class="cooked-difficulty-level"><strong class="cooked-meta-title">' . esc_html__('Difficulty','cooked') . '</strong>' . Cooked_Recipes::difficulty_level( $recipe['difficulty_level'] ) . '</span>';
700 echo wp_kses_post( apply_filters( 'cooked_show_difficulty_level', $dl_html, $recipe['difficulty_level'] ) );
701 }
702 }
703
704 public static function cooked_info_servings( $recipe ) {
705 global $_cooked_settings;
706
707 if (in_array('servings', $_cooked_settings['recipe_info_display_options'])) {
708 $servings = isset($recipe['nutrition']['servings']) && $recipe['nutrition']['servings'] ? $recipe['nutrition']['servings'] : 1;
709 Cooked_Recipes::serving_size_switcher( $servings );
710 Cooked_Recipes::measurement_system_switcher();
711 }
712 }
713
714 public static function cooked_info_print() {
715 global $recipe_settings, $_cooked_settings;
716
717 $recipe_post_url = get_permalink( $recipe_settings['id'] );
718 $query_args['print'] = 1;
719 $servings = (float)esc_html( get_query_var( 'servings', false ) );
720 $query_args['servings'] = !empty($servings) ? $servings : false;
721 echo '<span class="cooked-print"><a aria-label="' . esc_attr__('Print', 'cooked') . '" target="_blank" rel="nofollow" href="' . esc_url( add_query_arg( $query_args, $recipe_post_url ) ) . '" class="cooked-print-icon"><i class="cooked-icon cooked-icon-print"></i></a></span>';
722 }
723
724 public static function cooked_info_fullscreen() {
725 global $recipe_settings, $_cooked_settings;
726
727 echo '<span aria-label="' . esc_attr__('Fullscreen', 'cooked') . '" role="button" class="cooked-fsm-button" data-recipe-id="' . esc_attr( $recipe_settings['id'] ) . '"><i class="cooked-icon cooked-icon-fullscreen"></i></span>';
728 wp_enqueue_script('cooked-nosleep');
729 }
730
731 public static function cooked_info_prep_time( $recipe ) {
732 global $_cooked_settings;
733
734 if (!empty($_cooked_settings['recipe_info_display_options']) && in_array('timing_prep',$_cooked_settings['recipe_info_display_options'])) {
735 $prep_time = isset($recipe['prep_time']) ? esc_html( $recipe['prep_time'] ) : 0;
736 echo $prep_time ? '<span class="cooked-prep-time cooked-time"><span class="cooked-time-icon"><i class="cooked-icon cooked-icon-clock"></i></span><strong class="cooked-meta-title">' . esc_html__('Prep Time','cooked') . '</strong>' . wp_kses_post( Cooked_Measurements::time_format( $prep_time ) ) . '</span>' : '';
737 }
738 }
739
740 public static function cooked_info_cook_time( $recipe ) {
741 global $_cooked_settings;
742
743 if (!empty($_cooked_settings['recipe_info_display_options']) && in_array('timing_cook', $_cooked_settings['recipe_info_display_options'])) {
744 $cook_time = isset($recipe['cook_time']) ? esc_html( $recipe['cook_time'] ) : 0;
745 echo $cook_time ? '<span class="cooked-cook-time cooked-time"><span class="cooked-time-icon"><i class="cooked-icon cooked-icon-clock"></i></span><strong class="cooked-meta-title">' . esc_html__('Cook Time','cooked') . '</strong>' . wp_kses_post( Cooked_Measurements::time_format( $cook_time ) ) . '</span>' : '';
746 }
747 }
748
749 public static function cooked_info_total_time( $recipe ) {
750 global $_cooked_settings;
751
752 if (!empty($_cooked_settings['recipe_info_display_options']) && in_array('timing_total',$_cooked_settings['recipe_info_display_options'])) {
753 $total_time = isset($recipe['total_time']) ? esc_html( $recipe['total_time'] ) : 0;
754
755 if ( $total_time ) {
756 echo $total_time ? '<span class="cooked-total-time cooked-time"><span class="cooked-time-icon"><i class="cooked-icon cooked-icon-clock"></i></span><strong class="cooked-meta-title">' . esc_html__('Total Time','cooked') . '</strong>' . wp_kses_post( Cooked_Measurements::time_format( $total_time ) ) . '</span>' : '';
757 } else {
758 $prep_time = isset($recipe['prep_time']) ? esc_html( $recipe['prep_time'] ) : 0;
759 $cook_time = isset($recipe['cook_time']) ? esc_html( $recipe['cook_time'] ) : 0;
760
761 if ( $prep_time && $cook_time ) {
762 $total_time = $prep_time + $cook_time;
763 echo $total_time ? '<span class="cooked-total-time cooked-time"><span class="cooked-time-icon"><i class="cooked-icon cooked-icon-clock"></i></span><strong class="cooked-meta-title">' . esc_html__('Total Time','cooked') . '</strong>' . wp_kses_post( Cooked_Measurements::time_format( $total_time ) ) . '</span>' : '';
764 }
765 }
766 }
767 }
768
769 public static function cooked_info_taxonomies() {
770 global $recipe_settings, $_cooked_settings, $clickable;
771
772 $clickable = isset($_cooked_settings['advanced']) && !empty($_cooked_settings['advanced']) && in_array( 'disable_public_recipes', $_cooked_settings['advanced'] ) ? false : true;
773
774 if (!empty($_cooked_settings['recipe_info_display_options']) && in_array('taxonomies', $_cooked_settings['recipe_info_display_options'])):
775
776 global $recipe_terms_list;
777 $recipe_terms_list = '';
778
779 do_action( 'cooked_info_taxonomies_shortcode_before', $recipe_settings );
780
781 if (!empty($_cooked_settings['recipe_taxonomies']) && in_array('cp_recipe_category', $_cooked_settings['recipe_taxonomies'])):
782 if ( $clickable ):
783 $recipe_terms_list .= get_the_term_list( $recipe_settings['id'], 'cp_recipe_category', '<span class="cooked-taxonomy cooked-category"><strong class="cooked-meta-title">' . __('Category','cooked') . '</strong>', ', ', '</span>' );
784 else:
785 $_recipe_terms_array = [];
786 $recipe_terms_list .= '<span class="cooked-taxonomy cooked-category"><strong class="cooked-meta-title">' . __('Category','cooked') . '</strong>';
787 $recipe_terms_array = wp_get_object_terms( $recipe_settings['id'], 'cp_recipe_category' );
788 if ( !empty($recipe_terms_array) ):
789 if ( ! is_wp_error( $recipe_terms_array ) ):
790 foreach( $recipe_terms_array as $recipe_term ):
791 $_recipe_terms_array[] = esc_html( $recipe_term->name );
792 endforeach;
793 endif;
794 endif;
795 $recipe_terms_list .= implode( ', ', $_recipe_terms_array );
796 $recipe_terms_list .= '</span>';
797 endif;
798 endif;
799
800 do_action( 'cooked_info_taxonomies_shortcode_after', $recipe_settings );
801
802 if ( $recipe_terms_list ):
803 echo wp_kses_post( $recipe_terms_list );
804 endif;
805
806 endif;
807 }
808
809 public function cooked_excerpt_shortcode($atts, $content = null) {
810 global $_cooked_settings, $recipe_settings;
811
812 if (isset($_cooked_settings['recipe_info_display_options']) && is_array($_cooked_settings['recipe_info_display_options']) && in_array('excerpt', $_cooked_settings['recipe_info_display_options'])) {
813 ob_start();
814
815 if (isset($recipe_settings['excerpt']) && $recipe_settings['excerpt']) {
816 $excerpt = Cooked_Recipes::format_content($recipe_settings['excerpt']);
817 echo '<div class="cooked-recipe-excerpt cooked-clearfix">' . do_shortcode($excerpt) . '</div>';
818 }
819
820 return ob_get_clean();
821 }
822 }
823
824 public function cooked_notes_shortcode($atts, $content = null) {
825 global $_cooked_settings, $recipe_settings;
826
827 // Shortcode Attributes
828 $atts = shortcode_atts([
829 'show_header' => false,
830 ], $atts);
831
832 $show_header = Cooked_Functions::sanitize_text_field($atts['show_header']);
833 $show_header = !$show_header || $show_header == 'false' ? false : $show_header;
834
835 if (isset($_cooked_settings['recipe_info_display_options']) && is_array($_cooked_settings['recipe_info_display_options']) && in_array('notes', $_cooked_settings['recipe_info_display_options'])) {
836 ob_start();
837
838 if (isset($recipe_settings['notes']) && !empty($recipe_settings['notes'])) {
839 $notes = Cooked_Recipes::format_content($recipe_settings['notes']);
840 $show_header = $show_header ? '<div class="cooked-heading">' . esc_html__( 'Notes', 'cooked' ) . '</div>' : '';
841
842 echo '<div class="cooked-recipe-notes cooked-clearfix">';
843 echo wp_kses_post( $show_header );
844 echo do_shortcode($notes);
845 echo '</div>';
846 }
847
848 return ob_get_clean();
849 }
850 }
851
852 public function cooked_title_shortcode($atts, $content = null) {
853 global $recipe, $recipe_settings;
854
855 if (!isset($recipe_settings['id'])) {
856 $recipe_settings['id'] = $recipe->ID;
857 }
858
859 return esc_html( get_the_title($recipe_settings['id']) );
860 }
861
862 public function cooked_image_shortcode($atts, $content = null) {
863 global $recipe, $recipe_settings;
864
865 if ( !isset($recipe_settings['id']) ):
866 $recipe_settings['id'] = $recipe->ID;
867 endif;
868
869 $recipe = get_post( $recipe_settings['id'] );
870 wp_reset_postdata();
871
872 ob_start();
873
874 if (has_post_thumbnail($recipe)) :
875 echo '<div class="cooked-post-featured-image">';
876 echo get_the_post_thumbnail( $recipe, 'cooked-large' );
877 echo '</div>';
878 endif;
879
880 return ob_get_clean();
881 }
882
883 public function cooked_ingredients_shortcode($atts, $content = null) {
884 global $recipe_settings;
885
886 // Shortcode Attributes
887 $atts = shortcode_atts([
888 'checkboxes' => true,
889 ], $atts);
890
891 $checkboxes = Cooked_Functions::sanitize_text_field($atts['checkboxes']);
892 $checkboxes = !$checkboxes || $checkboxes == 'false' ? false : $checkboxes;
893
894 ob_start();
895
896 do_action( 'cooked_ingredients_shortcode_before', $recipe_settings );
897
898 if ( isset($recipe_settings['ingredients']) && !empty($recipe_settings['ingredients']) ):
899 echo '<div class="cooked-recipe-ingredients">';
900 foreach ( $recipe_settings['ingredients'] as $ing ):
901 Cooked_Recipes::single_ingredient( $ing, $checkboxes );
902 endforeach;
903 echo '</div>';
904 endif;
905
906 do_action( 'cooked_ingredients_shortcode_after', $recipe_settings );
907
908 return ob_get_clean();
909 }
910
911 public function cooked_directions_shortcode($atts, $content = null) {
912 global $recipe_settings;
913
914 // Shortcode Attributes
915 $atts = shortcode_atts(apply_filters('cooked_directions_shortcode_atts', [
916 'numbers' => true,
917 ]), $atts);
918
919 $step = 0;
920 $numbers = Cooked_Functions::sanitize_text_field( $atts['numbers'] );
921 $numbers = !$numbers || $numbers == 'false' ? false : $numbers;
922
923 ob_start();
924
925 if (isset($recipe_settings['directions']) && !empty($recipe_settings['directions'])) {
926 echo '<div class="cooked-recipe-directions">';
927 foreach ($recipe_settings['directions'] as $dir) {
928 if (!isset($dir['section_heading_name'])) {
929 $step++;
930 $number = $numbers ? $step : false;
931 } else {
932 $number = 0;
933 }
934
935 Cooked_Recipes::single_direction($dir, $number, false, $step, $atts);
936 }
937 echo '</div>';
938 }
939
940 return ob_get_clean();
941 }
942
943 public function cooked_nutrition_shortcode($atts, $content = null) {
944 global $_cooked_settings, $recipe_settings;
945
946 // Shortcode Attributes
947 $atts = shortcode_atts([
948 'id' => false,
949 'float' => false,
950 ], $atts);
951
952 if ( isset( $atts['id'] ) && $atts['id'] ):
953 $recipe_settings = Cooked_Recipes::get( intval( $atts['id'] ), true );
954 else:
955 $recipe_settings = !empty($recipe_settings) ? $recipe_settings : false;
956 endif;
957
958 $float = Cooked_Functions::sanitize_text_field($atts['float']);
959
960 $_nf_fields = Cooked_Measurements::nutrition_facts();
961 $nutrition_facts = isset($recipe_settings['nutrition']) && !empty($recipe_settings['nutrition']) ? $recipe_settings['nutrition'] : false;
962
963 ob_start();
964
965 if ( $nutrition_facts ):
966
967 $servings_change = (float)esc_html( get_query_var( 'servings', $recipe_settings['nutrition']['servings'] ) );
968
969 $top_facts = $_nf_fields['top'];
970 if (!empty($top_facts)):
971
972 // Start output buffer for top facts.
973 ob_start();
974
975 echo '<div class="cooked-nut-servings">';
976 foreach ( $top_facts as $slug => $nf ):
977 if ( $slug === 'serving_size' ):
978 echo '<div class="cooked-serving-size"><strong>' . esc_html($nf['name']) . '</strong> ';
979 echo '<p class="cooked-right"><strong class="cooked-nut-label" data-labeltype="' . esc_attr($slug) . '">' . esc_html( isset($nutrition_facts[$slug]) ? $nutrition_facts[$slug] : '' ) . '</strong></p>';
980 echo '</div>';
981 else:
982 echo '<p><strong class="cooked-nut-label" data-labeltype="' . esc_attr( $slug ) . '">' . esc_html( $servings_change ) . '</strong> ' . esc_html(strtolower($nf['name'])) . '</p>';
983 endif;
984 endforeach;
985 echo '</div>';
986
987 // Get top facts content from buffer.
988 $top_facts_content = ob_get_clean();
989
990 endif;
991
992 $mid_facts = $_nf_fields['mid'];
993 if (!empty($mid_facts)):
994
995 // Start output buffer for mid-facts.
996 ob_start();
997
998 foreach ( $mid_facts as $slug => $nf ):
999 if ( isset( $nutrition_facts[$slug] ) && $nutrition_facts[$slug] || isset( $nutrition_facts[$slug] ) && $nutrition_facts[$slug] === '0' ):
1000 echo '<dt class="cooked-calories no-after"><strong>' . esc_html($nf['name']) . '</strong>';
1001 echo '<strong class="cooked-nut-label">' . esc_html( $nutrition_facts[$slug] ) . '</strong>';
1002 echo '</dt>';
1003 endif;
1004 endforeach;
1005
1006 // Get mid facts content from buffer.
1007 $mid_facts_content = ob_get_clean();
1008
1009 endif;
1010
1011 $main_facts = $_nf_fields['main'];
1012 $nut_loops = 0;
1013
1014 if (!empty($main_facts)):
1015
1016 // Start output buffer for main facts.
1017 ob_start();
1018
1019 foreach ( $main_facts as $slug => $nf ):
1020
1021 if ( isset( $nutrition_facts[$slug] ) && $nutrition_facts[$slug] || isset( $nutrition_facts[$slug] ) && $nutrition_facts[$slug] === '0' ):
1022
1023 echo '<dt>';
1024 echo '<strong>' . esc_html( $nf['name'] ) . '</strong> <strong class="cooked-nut-label">' . esc_html( $nutrition_facts[$slug] ) . '</strong>' . ( isset($nf['measurement']) ? '<strong class="cooked-nut-label cooked-nut-measurement">' . esc_html( $nf['measurement'] ) . '</strong>' : '' );
1025 echo ( isset( $nf['pdv'] ) && $nutrition_facts[$slug] ? '<strong class="cooked-nut-right"><span class="cooked-nut-percent">' . esc_html( ceil( ( $nutrition_facts[$slug] / $nf['pdv'] ) * 100 ) ) . '</span>%</strong>' : '' );
1026
1027 if ( isset($nf['subs']) ):
1028 foreach( $nf['subs'] as $sub_slug => $sub_nf ):
1029 if ( isset( $nutrition_facts[$sub_slug] ) && $nutrition_facts[$sub_slug] || isset( $nutrition_facts[$sub_slug] ) && $nutrition_facts[$sub_slug] === '0' ):
1030 echo '<dl>';
1031 if ($sub_slug === 'trans_fat'):
1032 echo '<dt>';
1033 echo wp_kses_post( $sub_nf['nutrition_info_name'] ) . ' <strong class="cooked-nut-label" data-labeltype="' . esc_attr( $sub_slug ) . '">' . esc_html( $nutrition_facts[$sub_slug] ) . '</strong>' . ( isset($sub_nf['measurement']) ? '<strong class="cooked-nut-label" data-labeltype="' . esc_attr( $sub_slug ) . '_measurement">' . esc_html( $sub_nf['measurement'] ) . '</strong>' : '' );
1034 echo '</dt>';
1035 elseif ($sub_slug === 'added_sugars'):
1036 echo '<dl><dt>';
1037 echo esc_html__('Includes', 'cooked') . ' <strong class="cooked-nut-label" data-labeltype="' . esc_attr( $sub_slug ) . '">' . esc_html( $nutrition_facts[$sub_slug] ) . '</strong>' . ( isset($sub_nf['measurement']) ? '<strong class="cooked-nut-label" data-labeltype="' . esc_attr( $sub_slug ) . '_measurement">' . esc_html( $sub_nf['measurement'] ) . '</strong>' : '' ) . ' ' . esc_html($sub_nf['name']);
1038 echo ( isset( $sub_nf['pdv'] ) ? '<strong class="cooked-nut-right"><span class="cooked-nut-percent" data-pdv="' . esc_attr($sub_nf['pdv']) . '" data-labeltype="' . esc_attr($sub_slug) . '">' . esc_html( ceil( ( $nutrition_facts[$sub_slug] / $sub_nf['pdv'] ) * 100 ) ) . '</span>%</strong>' : '' );
1039 echo '</dt></dl>';
1040 else:
1041 echo '<dt>';
1042 echo esc_html( $sub_nf['name'] ) . ' <strong class="cooked-nut-label">' . esc_html( $nutrition_facts[$sub_slug] ) . '</strong>' . ( isset($sub_nf['measurement']) ? '<strong class="cooked-nut-label cooked-nut-measurement">' . esc_html( $sub_nf['measurement'] ) . '</strong>' : '' );
1043 echo ( isset( $sub_nf['pdv'] ) && $nutrition_facts[$sub_slug] ? '<strong class="cooked-nut-right"><span class="cooked-nut-percent">' . esc_html( ceil( ( $nutrition_facts[$sub_slug] / $sub_nf['pdv'] ) * 100 ) ) . '</span>%</strong>' : '' );
1044 echo '</dt>';
1045 endif;
1046 echo '</dl>';
1047 endif;
1048 endforeach;
1049 endif;
1050
1051 echo '</dt>';
1052
1053 endif;
1054
1055 endforeach;
1056
1057 // Get main facts content from buffer.
1058 $main_facts_content = ob_get_clean();
1059
1060 endif;
1061
1062 $bottom_facts = $_nf_fields['bottom'];
1063
1064 if (!empty($bottom_facts)):
1065
1066 // Start output buffer for bottom facts.
1067 ob_start();
1068
1069 foreach ( $bottom_facts as $slug => $nf ):
1070 if ( isset( $nutrition_facts[$slug] ) && $nutrition_facts[$slug] || isset( $nutrition_facts[$slug] ) && $nutrition_facts[$slug] === '0' ):
1071 echo '<dt>';
1072 echo '<strong>' . esc_html($nf['name']) . '</strong> <span class="cooked-nut-label" data-labeltype="' . esc_attr( $slug ) . '">' . esc_html( $nutrition_facts[$slug] ) . '</span>' . ( isset($nf['measurement']) ? '<span class="cooked-nut-label" data-labeltype="' . esc_attr( $slug ) . '_measurement">' . esc_html( $nf['measurement'] ) . '</span>' : '' );
1073 echo ( isset( $nf['pdv'] ) ? '<strong class="cooked-nut-right"><span class="cooked-nut-percent" data-pdv="' . esc_attr($nf['pdv']) . '" data-labeltype="' . esc_attr($slug) . '">' . esc_html( ceil( ( $nutrition_facts[$slug] / $nf['pdv'] ) * 100 ) ) . '</span>%</strong>' : '' );
1074 echo '</dt>';
1075 endif;
1076 endforeach;
1077
1078 // Get bottom facts content from buffer.
1079 $bottom_facts_content = ob_get_clean();
1080
1081 endif;
1082
1083 // Start a buffer for all nutrition facts content
1084 ob_start();
1085
1086 if ( isset($top_facts_content) && $top_facts_content ):
1087 echo wp_kses_post( $top_facts_content );
1088 endif;
1089
1090 if ( isset($mid_facts_content) && $mid_facts_content || isset($main_facts_content) && $main_facts_content ):
1091
1092 echo '<hr class="cooked-nut-hr" />';
1093 echo '<dl>';
1094
1095 echo '<dt><strong class="cooked-nut-heading">' . esc_html__('Amount per serving','cooked') . '</strong></dt>';
1096
1097 if ( isset($mid_facts_content) && $mid_facts_content ):
1098 echo '<section class="cooked-clearfix">';
1099 echo wp_kses_post( $mid_facts_content );
1100 echo '</section>';
1101 endif;
1102
1103 if ( isset($main_facts_content) && $main_facts_content ):
1104 echo '<dt class="cooked-nut-spacer"></dt>';
1105 echo '<dt class="cooked-nut-no-border"><strong class="cooked-nut-heading cooked-nut-right">' . esc_html__('% Daily Value *','cooked'). '</strong></dt>';
1106 echo '<section class="cooked-clearfix">';
1107 echo wp_kses_post( $main_facts_content );
1108 echo '</section>';
1109 endif;
1110
1111 echo '</dl>';
1112 echo '<hr class="cooked-nut-hr" />';
1113
1114 endif;
1115
1116 if ( isset($bottom_facts_content) && $bottom_facts_content ):
1117 echo '<dl class="cooked-nut-bottom cooked-clearfix">';
1118 echo wp_kses_post( $bottom_facts_content );
1119 echo '</dl>';
1120 endif;
1121
1122 $nutrition_facts_content = ob_get_clean();
1123
1124 if ( isset($nutrition_facts_content) && $nutrition_facts_content ):
1125
1126 echo '<div class="cooked-nutrition-label' . ( $float ? ' cooked-float-'.esc_attr( $float ) : '' ) . '">';
1127 echo '<div class="cooked-nutrition-title">' . esc_html__('Nutrition Facts', 'cooked') . '</div>';
1128 echo wp_kses_post( $nutrition_facts_content );
1129 if ( isset($main_facts_content) && $main_facts_content || isset($bottom_facts_content) && $bottom_facts_content ):
1130 echo '<div class="cooked-nut-spacer"></div>';
1131 echo '<p class="cooked-daily-value-text">* ' . esc_html__('The % Daily Value (DV) tells you how much a nutrient in a serving of food contributes to a daily diet. 2,000 calories a day is used for general nutrition advice.','cooked') . '</p>';
1132 endif;
1133
1134 // Add the Edamam attribution "Powered by Edamam" if the Edamam API is enabled.
1135 // More Information: https://developer.edamam.com/attribution
1136 if (!empty($_cooked_settings['enable_nutrition_api'])) :
1137 echo '<div class="cooked-nutrition-edamam">';
1138 do_action( 'cooked_nutrition_facts_powered_by_edamam' );
1139 echo '</div>';
1140 endif;
1141
1142 echo '</div>';
1143
1144 endif;
1145
1146 endif;
1147
1148 return ob_get_clean();
1149 }
1150
1151 /**
1152 * Related Recipes Shortcode
1153 *
1154 * Displays related recipes based on shared terms in any recipe taxonomy (categories,
1155 * cuisines, cooking methods, tags, diets), random order. No scoring, ingredients, or cache.
1156 *
1157 * @param array $atts Shortcode attributes
1158 * @param string $content Shortcode content
1159 * @return string Related recipes HTML
1160 * @since 1.0.0
1161 */
1162 public function cooked_related_recipes_shortcode($atts, $content = null) {
1163 if ( is_admin() ) return false;
1164
1165 global $post, $_cooked_settings;
1166
1167 // Shortcode Attributes
1168 $atts = shortcode_atts( Cooked_Related_Recipes::get_default_atts(), $atts );
1169
1170 // Get recipe ID - validate if provided, otherwise try current post
1171 $recipe_id = false;
1172
1173 if (!empty($atts['id'])) {
1174 // Validate that the ID is numeric before converting
1175 if (is_numeric($atts['id']) && $atts['id'] > 0) {
1176 $recipe_id = (int) $atts['id'];
1177 } else {
1178 // Invalid ID provided (e.g., "asdas", "", "0", "-5")
1179 return '<p class="cooked-related-recipes-error">' . __('Invalid recipe ID specified. Please provide a valid numeric recipe ID.', 'cooked') . '</p>';
1180 }
1181 } else {
1182 // No ID provided, try to get from current post
1183 if (isset($post->ID) && get_post_type($post->ID) === 'cp_recipe') {
1184 $recipe_id = $post->ID;
1185 }
1186 }
1187
1188 if (!$recipe_id) {
1189 return '<p class="cooked-related-recipes-error">' . __('No recipe found. Please specify a recipe ID using the id attribute, or use this shortcode on a recipe page.', 'cooked') . '</p>';
1190 }
1191
1192 // Get the source recipe (allow any status to support drafts/pending)
1193 $source_recipe = Cooked_Recipes::get($recipe_id, true, false, false, false, 'any');
1194
1195 if (!$source_recipe || empty($source_recipe)) {
1196 $error_msg = __('Recipe not found.', 'cooked');
1197 // Check if it's a different post type
1198 $post_check = get_post($recipe_id);
1199 if ($post_check && $post_check->post_type !== 'cp_recipe') {
1200 /* translators: %d: post ID */
1201 $error_msg .= ' ' . sprintf(__('The specified ID (%d) is not a recipe.', 'cooked'), $recipe_id);
1202 } elseif (!$post_check) {
1203 /* translators: %d: post ID */
1204 $error_msg .= ' ' . sprintf(__('No post found with ID %d.', 'cooked'), $recipe_id);
1205 }
1206 return '<p class="cooked-related-recipes-error">' . esc_html($error_msg) . '</p>';
1207 }
1208
1209 // Find related recipes (on-demand query: taxonomies OR + orderby rand, no cache)
1210 $related_recipes = Cooked_Related_Recipes::get_related_recipes( $recipe_id, $atts );
1211
1212 if (empty($related_recipes)) {
1213 $empty_msg = __('No related recipes found.', 'cooked');
1214 // Provide helpful context
1215 if (class_exists('Cooked_Multilingual') && Cooked_Multilingual::is_multilingual_active()) {
1216 $empty_msg .= ' ' . __('This may be because there are no other recipes in the current language with matching attributes.', 'cooked');
1217 } else {
1218 $empty_msg .= ' ' . __('Try adjusting the matching criteria or ensure you have other published recipes with shared categories, tags, or ingredients.', 'cooked');
1219 }
1220 return '<p class="cooked-related-recipes-empty">' . $empty_msg . '</p>';
1221 }
1222
1223 // Get recipe IDs for display and filter out deleted/invalid recipes
1224 // Note: Language filtering is already handled by WP_Query in find_related_recipes()
1225 $recipe_ids = array_column($related_recipes, 'id');
1226
1227 /**
1228 * Filter recipe IDs before display.
1229 *
1230 * @since 1.12.0
1231 *
1232 * @param array $recipe_ids Array of recipe IDs to display.
1233 * @param array $related_recipes Full related recipes array (each item has 'id').
1234 * @param int $recipe_id Source recipe ID.
1235 * @param array $atts Shortcode attributes.
1236 */
1237 $recipe_ids = apply_filters( 'cooked_related_recipes_display_ids', $recipe_ids, $related_recipes, $recipe_id, $atts );
1238
1239 ob_start();
1240
1241 // Display title
1242 if ($atts['title']) {
1243 echo '<h3 class="cooked-related-recipes-title">' . esc_html($atts['title']) . '</h3>';
1244 }
1245
1246 // Display recipes in grid
1247 $columns = intval($atts['columns']);
1248 $hide_image = $atts['hide_image'] && $atts['hide_image'] !== 'false';
1249 $hide_excerpt = $atts['hide_excerpt'] && $atts['hide_excerpt'] !== 'false';
1250 $hide_author = $atts['hide_author'] && $atts['hide_author'] !== 'false';
1251
1252 echo '<div class="cooked-related-recipes-grid cooked-recipe-grid cooked-columns-' . esc_attr($columns) . '">';
1253
1254 foreach ($recipe_ids as $rid) {
1255 echo '<div class="cooked-recipe">';
1256 echo wp_kses_post( Cooked_Recipes::card($rid, false, $hide_image, false, $hide_excerpt, $hide_author) );
1257 echo '</div>';
1258 }
1259
1260 echo '</div>';
1261
1262 $output = ob_get_clean();
1263
1264 /**
1265 * Filter the related recipes shortcode output HTML.
1266 *
1267 * @since 1.12.0
1268 *
1269 * @param string $output The shortcode HTML output.
1270 * @param array $recipe_ids Array of recipe IDs being displayed.
1271 * @param int $recipe_id Source recipe ID.
1272 * @param array $atts Shortcode attributes.
1273 */
1274 return apply_filters( 'cooked_related_recipes_output', $output, $recipe_ids, $recipe_id, $atts );
1275 }
1276 }
1277