PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 All 508 releases
← All changes | modules/shortcodes/recipe.php +698 -0 16.2-beta → 16.3 View file →
@@ -1,0 +1,698 @@
1 +<?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 +
3 +use Automattic\Jetpack\Assets;
4 +
5 +if ( ! defined( 'ABSPATH' ) ) {
6 + exit( 0 );
7 +}
8 +
9 +/**
10 + * Embed recipe 'cards' in post, with basic styling and print functionality
11 + *
12 + * To Do
13 + * - defaults settings
14 + * - basic styles/themecolor styles
15 + * - validation/sanitization
16 + * - print styles
17 + *
18 + * @package automattic/jetpack
19 + */
20 +
21 +/**
22 + * Register and display Recipes in posts.
23 + *
24 + * @phan-constructor-used-for-side-effects
25 + */
26 +class Jetpack_Recipes {
27 +
28 + /**
29 + * Have scripts and styles been enqueued already.
30 + *
31 + * @var bool
32 + */
33 + private $scripts_and_style_included = false;
34 +
35 + /**
36 + * Constructor
37 + */
38 + public function __construct() {
39 + add_action( 'init', array( $this, 'action_init' ) );
40 + }
41 +
42 + /**
43 + * Returns KSES tags with Schema-specific attributes.
44 + *
45 + * @since 8.0.0
46 + *
47 + * @return array Array to be used by KSES.
48 + */
49 + private static function kses_tags() {
50 + $allowedtags = wp_kses_allowed_html( 'post' );
51 + // Create an array of all the tags we'd like to add the itemprop attribute to.
52 + $tags = array( 'li', 'ol', 'ul', 'img', 'p', 'h3', 'time', 'span' );
53 + foreach ( $tags as $tag ) {
54 + if ( ! isset( $allowedtags[ $tag ] ) ) {
55 + $allowedtags[ $tag ] = array();
56 + }
57 + $allowedtags[ $tag ]['class'] = array();
58 + $allowedtags[ $tag ]['itemprop'] = array();
59 + $allowedtags[ $tag ]['datetime'] = array();
60 + }
61 +
62 + // Allow the handler <a on=""> in AMP.
63 + $allowedtags['a']['on'] = array();
64 +
65 + // Allow itemscope and itemtype for divs.
66 + if ( ! isset( $allowedtags['div'] ) ) {
67 + $allowedtags['div'] = array();
68 + }
69 + $allowedtags['div']['class'] = array();
70 + $allowedtags['div']['itemscope'] = array();
71 + $allowedtags['div']['itemtype'] = array();
72 + return $allowedtags;
73 + }
74 +
75 + /**
76 + * Register our shortcode and enqueue necessary files.
77 + */
78 + public function action_init() {
79 + // Enqueue styles if [recipe] exists.
80 + add_action( 'wp_head', array( $this, 'add_scripts' ), 1 );
81 +
82 + // Render [recipe], along with other shortcodes that can be nested within.
83 + add_shortcode( 'recipe', array( $this, 'recipe_shortcode' ) );
84 + add_shortcode( 'recipe-notes', array( $this, 'recipe_notes_shortcode' ) );
85 + add_shortcode( 'recipe-ingredients', array( $this, 'recipe_ingredients_shortcode' ) );
86 + add_shortcode( 'recipe-directions', array( $this, 'recipe_directions_shortcode' ) );
87 + add_shortcode( 'recipe-nutrition', array( $this, 'recipe_nutrition_shortcode' ) );
88 + add_shortcode( 'recipe-image', array( $this, 'recipe_image_shortcode' ) );
89 + }
90 +
91 + /**
92 + * Enqueue scripts and styles
93 + */
94 + public function add_scripts() {
95 + if ( empty( $GLOBALS['posts'] ) || ! is_array( $GLOBALS['posts'] ) ) {
96 + return;
97 + }
98 +
99 + foreach ( $GLOBALS['posts'] as $p ) {
100 + if ( isset( $p->post_content ) && has_shortcode( $p->post_content, 'recipe' ) ) {
101 + $this->scripts_and_style_included = true;
102 + break;
103 + }
104 + }
105 +
106 + if ( ! $this->scripts_and_style_included ) {
107 + return;
108 + }
109 +
110 + wp_enqueue_style( 'jetpack-recipes-style', plugins_url( '/css/recipes.css', __FILE__ ), array(), '20130919' );
111 + wp_style_add_data( 'jetpack-recipes-style', 'rtl', 'replace' );
112 +
113 + // add $themecolors-defined styles.
114 + wp_add_inline_style( 'jetpack-recipes-style', self::themecolor_styles() );
115 +
116 + if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
117 + return;
118 + }
119 +
120 + wp_register_script(
121 + 'jetpack-shortcode-deps',
122 + plugins_url( '_inc/build/shortcodes/js/dependencies.min.js', JETPACK__PLUGIN_FILE ),
123 + array( 'jquery' ),
124 + '20250905',
125 + true
126 + );
127 +
128 + wp_enqueue_script(
129 + 'jetpack-recipes-js',
130 + Assets::get_file_url_for_environment( '_inc/build/shortcodes/js/recipes.min.js', 'modules/shortcodes/js/recipes.js' ),
131 + array( 'jquery', 'jetpack-shortcode-deps' ),
132 + '20131230',
133 + false
134 + );
135 +
136 + $title_var = wp_title( '|', false, 'right' );
137 + $rtl = is_rtl() ? '-rtl' : '';
138 + $print_css_var = plugins_url( "/css/recipes-print{$rtl}.css", __FILE__ );
139 +
140 + wp_localize_script(
141 + 'jetpack-recipes-js',
142 + 'jetpack_recipes_vars',
143 + array(
144 + 'pageTitle' => $title_var,
145 + 'loadCSS' => $print_css_var,
146 + )
147 + );
148 + }
149 +
150 + /**
151 + * Our [recipe] shortcode.
152 + * Prints recipe data styled to look good on *any* theme.
153 + *
154 + * @param array $atts Array of shortcode attributes.
155 + * @param string $content Post content.
156 + *
157 + * @return string HTML for recipe shortcode.
158 + */
159 + public static function recipe_shortcode( $atts, $content = '' ) {
160 + $atts = shortcode_atts(
161 + array(
162 + 'title' => '', // string.
163 + 'servings' => '', // intval.
164 + 'time' => '', // strtotime-compatible time description.
165 + 'difficulty' => '', // string.
166 + 'print' => '', // URL for external print version.
167 + 'source' => '', // string.
168 + 'sourceurl' => '', // URL string. Only used if source set.
169 + 'image' => '', // URL or attachment ID.
170 + 'description' => '', // string.
171 + 'cooktime' => '', // strtotime-compatible time description.
172 + 'preptime' => '', // strtotime-compatible time description.
173 + 'rating' => '', // string.
174 + ),
175 + $atts,
176 + 'recipe'
177 + );
178 +
179 + return self::recipe_shortcode_html( $atts, $content );
180 + }
181 +
182 + /**
183 + * The recipe output
184 + *
185 + * @param array $atts Array of shortcode attributes.
186 + * @param string $content Post content.
187 + *
188 + * @return string HTML output
189 + */
190 + private static function recipe_shortcode_html( $atts, $content = '' ) {
191 +
192 + $html = '<div class="hrecipe h-recipe jetpack-recipe" itemscope itemtype="https://schema.org/Recipe">';
193 +
194 + // Print the recipe title if exists.
195 + if ( '' !== $atts['title'] ) {
196 + $html .= '<h3 class="p-name jetpack-recipe-title fn" itemprop="name">' . esc_html( $atts['title'] ) . '</h3>';
197 + }
198 +
199 + // Print the recipe meta if exists.
200 + if (
201 + '' !== $atts['servings']
202 + || '' !== $atts['time']
203 + || '' !== $atts['difficulty']
204 + || '' !== $atts['print']
205 + || '' !== $atts['preptime']
206 + || '' !== $atts['cooktime']
207 + || '' !== $atts['rating']
208 + ) {
209 + $html .= '<ul class="jetpack-recipe-meta">';
210 +
211 + if ( '' !== $atts['servings'] ) {
212 + $html .= sprintf(
213 + '<li class="jetpack-recipe-servings p-yield yield" itemprop="recipeYield"><strong>%1$s: </strong>%2$s</li>',
214 + esc_html_x( 'Servings', 'recipe', 'jetpack' ),
215 + esc_html( $atts['servings'] )
216 + );
217 + }
218 +
219 + $time_types = array( 'preptime', 'cooktime', 'time' );
220 + foreach ( $time_types as $time_type ) {
221 + if ( '' === $atts[ $time_type ] ) {
222 + continue;
223 + }
224 + $html .= self::output_time( $atts[ $time_type ], $time_type );
225 + }
226 +
227 + if ( '' !== $atts['difficulty'] ) {
228 + $html .= sprintf(
229 + '<li class="jetpack-recipe-difficulty"><strong>%1$s: </strong>%2$s</li>',
230 + esc_html_x( 'Difficulty', 'recipe', 'jetpack' ),
231 + esc_html( $atts['difficulty'] )
232 + );
233 + }
234 +
235 + if ( '' !== $atts['rating'] ) {
236 + $html .= sprintf(
237 + '<li class="jetpack-recipe-rating">
238 + <strong>%1$s: </strong>
239 + <span itemprop="contentRating">%2$s</span>
240 + </li>',
241 + esc_html_x( 'Rating', 'recipe', 'jetpack' ),
242 + esc_html( $atts['rating'] )
243 + );
244 + }
245 +
246 + if ( '' !== $atts['source'] ) {
247 + $html .= sprintf(
248 + '<li class="jetpack-recipe-source"><strong>%1$s: </strong>',
249 + esc_html_x( 'Source', 'recipe', 'jetpack' )
250 + );
251 +
252 + if ( '' !== $atts['sourceurl'] ) :
253 + // Show the link if we have one.
254 + $html .= sprintf(
255 + '<a href="%2$s">%1$s</a>',
256 + esc_html( $atts['source'] ),
257 + esc_url( $atts['sourceurl'] )
258 + );
259 + else :
260 + // Skip the link.
261 + $html .= sprintf(
262 + '%1$s',
263 + esc_html( $atts['source'] )
264 + );
265 + endif;
266 +
267 + $html .= '</li>';
268 + }
269 +
270 + if ( 'false' !== $atts['print'] ) {
271 + $is_amp = class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request();
272 + $print_action = $is_amp ? 'on="tap:AMP.print"' : '';
273 + $print_text = $is_amp ? esc_html__( 'Print page', 'jetpack' ) : esc_html_x( 'Print', 'recipe', 'jetpack' );
274 + $html .= sprintf(
275 + '<li class="jetpack-recipe-print"><a href="#" %1$s>%2$s</a></li>',
276 + $print_action,
277 + $print_text
278 + );
279 + }
280 +
281 + $html .= '</ul>';
282 + }
283 +
284 + // Output the image if we have one and it's not shown elsewhere.
285 + if ( '' !== $atts['image'] ) {
286 + if ( ! has_shortcode( $content, 'recipe-image' ) ) {
287 + $html .= self::output_image_html( $atts['image'] );
288 + }
289 + }
290 +
291 + // Output the description, if we have one.
292 + if ( '' !== $atts['description'] ) {
293 + $html .= sprintf(
294 + '<p class="jetpack-recipe-description" itemprop="description">%1$s</p>',
295 + esc_html( $atts['description'] )
296 + );
297 + }
298 +
299 + // Print content between codes.
300 + $html .= '<div class="jetpack-recipe-content">' . do_shortcode( $content ) . '</div>';
301 +
302 + // Close it up.
303 + $html .= '</div>';
304 +
305 + // If there is a recipe within a recipe, remove the shortcode.
306 + if ( has_shortcode( $html, 'recipe' ) ) {
307 + remove_shortcode( 'recipe' );
308 + }
309 +
310 + // Sanitize html.
311 + $html = wp_kses( $html, self::kses_tags() );
312 +
313 + // Return the HTML block.
314 + return $html;
315 + }
316 +
317 + /**
318 + * Our [recipe-image] shortcode.
319 + * Controls placement of image in recipe.
320 + *
321 + * @param array $atts Array of shortcode attributes.
322 + *
323 + * @return string HTML for recipe notes shortcode.
324 + */
325 + public static function recipe_image_shortcode( $atts ) {
326 + $atts = shortcode_atts(
327 + array(
328 + 'image' => '', // string.
329 + 0 => '', // string.
330 + ),
331 + $atts,
332 + 'recipe-image'
333 + );
334 + $src = $atts['image'];
335 + if ( ! empty( $atts[0] ) ) {
336 + $src = $atts[0];
337 + }
338 + return self::output_image_html( $src );
339 + }
340 +
341 + /**
342 + * Our [recipe-notes] shortcode.
343 + * Outputs ingredients, styled in a div.
344 + *
345 + * @param array $atts Array of shortcode attributes.
346 + * @param string $content Post content.
347 + *
348 + * @return string HTML for recipe notes shortcode.
349 + */
350 + public static function recipe_notes_shortcode( $atts, $content = '' ) {
351 + $atts = shortcode_atts(
352 + array(
353 + 'title' => '', // string.
354 + ),
355 + $atts,
356 + 'recipe-notes'
357 + );
358 +
359 + $html = '';
360 +
361 + // Print a title if one exists.
362 + if ( '' !== $atts['title'] ) {
363 + $html .= '<h4 class="jetpack-recipe-notes-title">' . esc_html( $atts['title'] ) . '</h4>';
364 + }
365 +
366 + $html .= '<div class="jetpack-recipe-notes">';
367 +
368 + // Format content using list functionality, if desired.
369 + $html .= self::output_list_content( $content, 'notes' );
370 +
371 + $html .= '</div>';
372 +
373 + // Sanitize html.
374 + $html = wp_kses( $html, self::kses_tags() );
375 +
376 + // Return the HTML block.
377 + return $html;
378 + }
379 +
380 + /**
381 + * Our [recipe-ingredients] shortcode.
382 + * Outputs notes, styled in a div.
383 + *
384 + * @param array $atts Array of shortcode attributes.
385 + * @param string $content Post content.
386 + *
387 + * @return string HTML for recipe ingredients shortcode.
388 + */
389 + public static function recipe_ingredients_shortcode( $atts, $content = '' ) {
390 + $atts = shortcode_atts(
391 + array(
392 + 'title' => esc_html_x( 'Ingredients', 'recipe', 'jetpack' ), // string.
393 + ),
394 + $atts,
395 + 'recipe-ingredients'
396 + );
397 +
398 + $html = '<div class="jetpack-recipe-ingredients">';
399 +
400 + // Print a title unless the user has opted to exclude it.
401 + if ( 'false' !== $atts['title'] ) {
402 + $html .= '<h4 class="jetpack-recipe-ingredients-title">' . esc_html( $atts['title'] ) . '</h4>';
403 + }
404 +
405 + // Format content using list functionality.
406 + $html .= self::output_list_content( $content, 'ingredients' );
407 +
408 + $html .= '</div>';
409 +
410 + // Sanitize html.
411 + $html = wp_kses( $html, self::kses_tags() );
412 +
413 + // Return the HTML block.
414 + return $html;
415 + }
416 +
417 + /**
418 + * Our [recipe-nutrition] shortcode.
419 + * Outputs notes, styled in a div.
420 + *
421 + * @param array $atts Array of shortcode attributes.
422 + * @param string $content Post content.
423 + *
424 + * @return string HTML for recipe nutrition shortcode.
425 + */
426 + public static function recipe_nutrition_shortcode( $atts, $content = '' ) {
427 + $atts = shortcode_atts(
428 + array(
429 + 'title' => esc_html_x( 'Nutrition', 'recipe', 'jetpack' ), // string.
430 + ),
431 + $atts,
432 + 'recipe-nutrition'
433 + );
434 +
435 + $html = '<div class="jetpack-recipe-nutrition p-nutrition nutrition">';
436 +
437 + // Print a title unless the user has opted to exclude it.
438 + if ( 'false' !== $atts['title'] ) {
439 + $html .= '<h4 class="jetpack-recipe-nutrition-title">' . esc_html( $atts['title'] ) . '</h4>';
440 + }
441 +
442 + // Format content using list functionality.
443 + $html .= self::output_list_content( $content, 'nutrition' );
444 +
445 + $html .= '</div>';
446 +
447 + // Sanitize html.
448 + $html = wp_kses( $html, self::kses_tags() );
449 +
450 + // Return the HTML block.
451 + return $html;
452 + }
453 +
454 + /**
455 + * Reusable function to check for shortened formatting.
456 + * Basically, users can create lists with the following shorthand:
457 + * - item one
458 + * - item two
459 + * - item three
460 + * And we'll magically convert it to a list. This has the added benefit
461 + * of including itemprops for the recipe schema.
462 + *
463 + * @param string $content HTML content.
464 + * @param string $type Type of list.
465 + *
466 + * @return string content formatted as a list item
467 + */
468 + private static function output_list_content( $content, $type ) {
469 + $html = '';
470 +
471 + switch ( $type ) {
472 + case 'directions':
473 + $list_item_replacement = '<li class="jetpack-recipe-directions">${1}</li>';
474 + $itemprop = ' itemprop="recipeInstructions"';
475 + $listtype = 'ol';
476 + break;
477 + case 'ingredients':
478 + $list_item_replacement = '<li class="jetpack-recipe-ingredient p-ingredient ingredient" itemprop="recipeIngredient">${1}</li>';
479 + $itemprop = '';
480 + $listtype = 'ul';
481 + break;
482 + case 'nutrition':
483 + $list_item_replacement = '<li class="jetpack-recipe-nutrition">${1}</li>';
484 + $itemprop = ' itemprop="nutrition"';
485 + $listtype = 'ul';
486 + break;
487 + default:
488 + $list_item_replacement = '<li class="jetpack-recipe-notes">${1}</li>';
489 + $itemprop = '';
490 + $listtype = 'ul';
491 + }
492 +
493 + // Check to see if the user is trying to use shortened formatting.
494 + if (
495 + str_contains( $content, '&#8211;' ) ||
496 + str_contains( $content, '&#8212;' ) ||
497 + str_contains( $content, '-' ) ||
498 + str_contains( $content, '*' ) ||
499 + str_contains( $content, '#' ) ||
500 + str_contains( $content, '–' ) || // ndash.
501 + str_contains( $content, '—' ) || // mdash.
502 + preg_match( '/\d+\.\s/', $content )
503 + ) {
504 + // Remove breaks and extra whitespace.
505 + $content = str_replace( "<br />\n", "\n", $content );
506 + $content = trim( $content );
507 +
508 + $ul_pattern = '/(?:^|\n|\<p\>)+(?:[\-–—]+|\&#8211;|\&#8212;|\*)+\h+(.*)/mi';
509 + $ol_pattern = '/(?:^|\n|\<p\>)+(?:\d+\.|#+)+\h+(.*)/mi';
510 +
511 + preg_match_all( $ul_pattern, $content, $ul_matches );
512 + preg_match_all( $ol_pattern, $content, $ol_matches );
513 +
514 + if ( ( is_countable( $ul_matches[0] ) && count( $ul_matches[0] ) > 0 ) || ( is_countable( $ol_matches[0] ) && count( $ol_matches[0] ) > 0 ) ) {
515 +
516 + if ( is_countable( $ol_matches[0] ) && count( $ol_matches[0] ) > 0 ) {
517 + $listtype = 'ol';
518 + $list_item_pattern = $ol_pattern;
519 + } else {
520 + $listtype = 'ul';
521 + $list_item_pattern = $ul_pattern;
522 + }
523 + $html .= '<' . $listtype . $itemprop . '>';
524 + $html .= preg_replace( $list_item_pattern, $list_item_replacement, $content );
525 + $html .= '</' . $listtype . '>';
526 +
527 + // Strip out any empty <p> tags and stray </p> tags, because those are just silly.
528 + $empty_p_pattern = '/(<p>)*\s*<\/p>/mi';
529 + $html = preg_replace( $empty_p_pattern, '', $html );
530 + } else {
531 + $html .= do_shortcode( $content );
532 + }
533 + } else {
534 + $html .= do_shortcode( $content );
535 + }
536 +
537 + // Return our formatted content.
538 + return $html;
539 + }
540 +
541 + /**
542 + * Our [recipe-directions] shortcode.
543 + * Outputs directions, styled in a div.
544 + *
545 + * @param array $atts Array of shortcode attributes.
546 + * @param string $content Post content.
547 + *
548 + * @return string HTML for recipe directions shortcode.
549 + */
550 + public static function recipe_directions_shortcode( $atts, $content = '' ) {
551 + $atts = shortcode_atts(
552 + array(
553 + 'title' => esc_html_x( 'Directions', 'recipe', 'jetpack' ), // string.
554 + ),
555 + $atts,
556 + 'recipe-directions'
557 + );
558 +
559 + $html = '<div class="jetpack-recipe-directions e-instructions">';
560 +
561 + // Print a title unless the user has specified to exclude it.
562 + if ( 'false' !== $atts['title'] ) {
563 + $html .= '<h4 class="jetpack-recipe-directions-title">' . esc_html( $atts['title'] ) . '</h4>';
564 + }
565 +
566 + // Format content using list functionality.
567 + $html .= self::output_list_content( $content, 'directions' );
568 +
569 + $html .= '</div>';
570 +
571 + // Sanitize html.
572 + $html = wp_kses( $html, self::kses_tags() );
573 +
574 + // Return the HTML block.
575 + return $html;
576 + }
577 +
578 + /**
579 + * Outputs time meta tag.
580 + *
581 + * @param string $time_str Raw time to output.
582 + * @param string $time_type Type of time to show.
583 + *
584 + * @return string HTML for recipe time meta.
585 + */
586 + private static function output_time( $time_str, $time_type ) {
587 + // Get a time that's supported by Schema.org.
588 + $duration = WPCOM_JSON_API_Date::format_duration( $time_str );
589 + // If no duration can be calculated, let's output what the user provided.
590 + if ( ! $duration ) {
591 + $duration = $time_str;
592 + }
593 +
594 + switch ( $time_type ) {
595 + case 'cooktime':
596 + $title = _x( 'Cook Time', 'recipe', 'jetpack' );
597 + $itemprop = 'cookTime';
598 + break;
599 + case 'preptime':
600 + $title = _x( 'Prep Time', 'recipe', 'jetpack' );
601 + $itemprop = 'prepTime';
602 + break;
603 + default:
604 + $title = _x( 'Time', 'recipe', 'jetpack' );
605 + $itemprop = 'totalTime';
606 + break;
607 + }
608 +
609 + return sprintf(
610 + '<li class="jetpack-recipe-%3$s">
611 + <time itemprop="%4$s" datetime="%5$s"><strong>%1$s:</strong> <span class="%3$s">%2$s</span></time>
612 + </li>',
613 + esc_html( $title ),
614 + esc_html( $time_str ),
615 + esc_attr( $time_type ),
616 + esc_attr( $itemprop ),
617 + esc_attr( $duration )
618 + );
619 + }
620 +
621 + /**
622 + * Outputs image tag for recipe.
623 + *
624 + * @param string $src The image source.
625 + *
626 + * @return string
627 + */
628 + private static function output_image_html( $src ) {
629 + // Exit if there is no provided source.
630 + if ( ! $src ) {
631 + return '';
632 + }
633 +
634 + $image_attrs = array(
635 + 'class' => 'jetpack-recipe-image u-photo photo',
636 + 'itemprop' => 'image',
637 + );
638 +
639 + if ( wp_lazy_loading_enabled( 'img', 'wp_get_attachment_image' ) ) {
640 + $image_attrs['loading'] = 'lazy';
641 + }
642 +
643 + // If it's numeric, this may be an attachment.
644 + if ( is_numeric( $src ) ) {
645 + return wp_get_attachment_image(
646 + $src,
647 + 'full',
648 + false,
649 + $image_attrs
650 + );
651 + }
652 +
653 + // Check if it's an absolute or relative URL, and return if not.
654 + if (
655 + ! str_starts_with( $src, '/' )
656 + && false === filter_var( $src, FILTER_VALIDATE_URL )
657 + ) {
658 + return '';
659 + }
660 +
661 + $image_attrs_markup = '';
662 + foreach ( $image_attrs as $name => $value ) {
663 + $image_attrs_markup .= sprintf(
664 + ' %1$s="%2$s"',
665 + esc_attr( $name ),
666 + esc_attr( $value )
667 + );
668 + }
669 +
670 + return sprintf(
671 + '<img%1$s src="%2$s" />',
672 + $image_attrs_markup,
673 + esc_url( $src )
674 + );
675 + }
676 +
677 + /**
678 + * Use $themecolors array to style the Recipes shortcode
679 + *
680 + * @print style block
681 + * @return string $style
682 + */
683 + public function themecolor_styles() {
684 + global $themecolors;
685 + $style = '';
686 +
687 + if ( isset( $themecolors['border'] ) ) {
688 + $style .= '.jetpack-recipe { border-color: #' . esc_attr( $themecolors['border'] ) . '; }';
689 + }
690 + if ( isset( $themecolors['link'] ) ) {
691 + $style .= '.jetpack-recipe-title { border-bottom-color: #' . esc_attr( $themecolors['link'] ) . '; }';
692 + }
693 +
694 + return $style;
695 + }
696 +}
697 +
698 +new Jetpack_Recipes();