PluginProbe
Cooked – Recipe Management / trunk
Cooked – Recipe Management vtrunk
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 1.8.5 All 36 releases
cooked / includes / class.cooked-shortcodes.php

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

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