Completed
Push — add/recipes-block ( f75780 )
by Jeremy
11:28
created

Jetpack_Recipes::init_block()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 0
dl 0
loc 46
rs 9.1781
c 0
b 0
f 0
1
<?php
2
3
/**
4
 * Embed recipe 'cards' in post, with basic styling and print functionality
5
 *
6
 * To Do
7
 * - defaults settings
8
 * - basic styles/themecolor styles
9
 * - validation/sanitization
10
 * - print styles
11
 */
12
class Jetpack_Recipes {
13
14
	private $scripts_and_style_included = false;
15
16
	function __construct() {
17
		add_action( 'init', array( $this, 'action_init' ) );
18
19
		// Create our Gutenberg block.
20
		add_action( 'init', array( $this, 'init_block' ) );
21
22
		add_filter( 'wp_kses_allowed_html', array( $this, 'add_recipes_kses_rules' ), 10, 2 );
23
	}
24
25
	/**
26
	 * Add Schema-specific attributes to our allowed tags in wp_kses,
27
	 * so we can have better Schema.org compliance.
28
	 *
29
	 * @param array $allowedtags Array of allowed HTML tags in recipes.
30
	 * @param array $context Context to judge allowed tags by.
31
	 */
32
	function add_recipes_kses_rules( $allowedtags, $context ) {
33
		if ( in_array( $context, array( '', 'post', 'data' ) ) ) :
34
			// Create an array of all the tags we'd like to add the itemprop attribute to.
35
			$tags = array( 'li', 'ol', 'ul', 'img', 'p', 'h3', 'time' );
36
			foreach ( $tags as $tag ) {
37
				$allowedtags = $this->add_kses_rule(
38
					$allowedtags,
39
					$tag,
40
					array(
41
						'class'    => array(),
42
						'itemprop' => array(),
43
						'datetime' => array(),
44
					)
45
				);
46
			}
47
48
			// Allow itemscope and itemtype for divs.
49
			$allowedtags = $this->add_kses_rule(
50
				$allowedtags,
51
				'div',
52
				array(
53
					'class'     => array(),
54
					'itemscope' => array(),
55
					'itemtype'  => array(),
56
				)
57
			);
58
		endif;
59
60
		return $allowedtags;
61
	}
62
63
	/**
64
	 * Function to add a new property rule to our kses array.
65
	 * Used by add_recipe_kses_rules() above.
66
	 *
67
	 * @param array  $all_tags Array of allowed HTML tags in recipes.
68
	 * @param string $tag      New HTML tag to add to the array of allowed HTML.
69
	 * @param array  $rules    Array of allowed attributes for that HTML tag.
70
	 */
71
	private function add_kses_rule( $all_tags, $tag, $rules ) {
72
73
		// If the tag doesn't already exist, add it.
74
		if ( ! isset( $all_tags[ $tag ] ) ) {
75
			$all_tags[ $tag ] = array();
76
		}
77
78
		// Merge the new tags with existing tags.
79
		$all_tags[ $tag ] = array_merge( $all_tags[ $tag ], $rules );
80
81
		return $all_tags;
82
	}
83
84
	/**
85
	 * Register our shortcode and enqueue necessary files.
86
	 */
87
	function action_init() {
88
		// Enqueue styles if [recipe] exists.
89
		add_action( 'wp_head', array( $this, 'add_scripts' ), 1 );
90
91
		// Render [recipe], along with other shortcodes that can be nested within.
92
		add_shortcode( 'recipe', array( $this, 'recipe_shortcode' ) );
93
		add_shortcode( 'recipe-notes', array( $this, 'recipe_notes_shortcode' ) );
94
		add_shortcode( 'recipe-ingredients', array( $this, 'recipe_ingredients_shortcode' ) );
95
		add_shortcode( 'recipe-directions', array( $this, 'recipe_directions_shortcode' ) );
96
	}
97
98
	/**
99
	 * Enqueue scripts and styles
100
	 */
101
	function add_scripts() {
102
		if ( empty( $GLOBALS['posts'] ) || ! is_array( $GLOBALS['posts'] ) ) {
103
			return;
104
		}
105
106
		foreach ( $GLOBALS['posts'] as $p ) {
107
			if ( has_shortcode( $p->post_content, 'recipe' ) ) {
108
				$this->scripts_and_style_included = true;
109
				break;
110
			}
111
		}
112
113
		if ( ! $this->scripts_and_style_included ) {
114
			return;
115
		}
116
117
		wp_enqueue_style( 'jetpack-recipes-style', plugins_url( '/css/recipes.css', __FILE__ ), array(), '20130919' );
118
		wp_style_add_data( 'jetpack-recipes-style', 'rtl', 'replace' );
119
120
		// add $themecolors-defined styles.
121
		wp_add_inline_style( 'jetpack-recipes-style', self::themecolor_styles() );
122
123
		wp_enqueue_script(
124
			'jetpack-recipes-printthis',
125
			Jetpack::get_file_url_for_environment( '_inc/build/shortcodes/js/recipes-printthis.min.js', 'modules/shortcodes/js/recipes-printthis.js' ),
126
			array( 'jquery' ),
127
			'20170202'
128
		);
129
130
		wp_enqueue_script(
131
			'jetpack-recipes-js',
132
			Jetpack::get_file_url_for_environment( '_inc/build/shortcodes/js/recipes.min.js', 'modules/shortcodes/js/recipes.js' ),
133
			array( 'jquery', 'jetpack-recipes-printthis' ),
134
			'20131230'
135
		);
136
137
		$title_var     = wp_title( '|', false, 'right' );
138
		$rtl           = is_rtl() ? '-rtl' : '';
139
		$print_css_var = plugins_url( "/css/recipes-print{$rtl}.css", __FILE__ );
140
141
		wp_localize_script(
142
			'jetpack-recipes-js',
143
			'jetpack_recipes_vars',
144
			array(
145
				'pageTitle' => $title_var,
146
				'loadCSS'   => $print_css_var,
147
			)
148
		);
149
	}
150
151
	/**
152
	 * Register Gutenberg Block element.
153
	 */
154
	public static function init_block() {
155
		// Bail early if Gutenberg is not installed.
156
		if ( ! function_exists( 'register_block_type' ) ) {
157
			return;
158
		}
159
160
		wp_register_script(
161
			'jetpack-recipes-block-editor',
162
			Jetpack::get_file_url_for_environment( '_inc/blocks/editor.js', '_inc/blocks/editor.js' ),
163
			array( 'wp-element', 'wp-blocks', 'wp-editor', 'wp-components', 'wp-i18n' ),
164
			JETPACK__VERSION,
165
			false
166
		);
167
168
		wp_register_script(
169
			'jetpack-recipes-block-view',
170
			Jetpack::get_file_url_for_environment( '_inc/blocks/view.js', '_inc/blocks/view.js' ),
171
			array( 'wp-element', 'wp-blocks', 'wp-editor', 'wp-components', 'wp-i18n' ),
172
			JETPACK__VERSION,
173
			false
174
		);
175
176
		wp_register_style(
177
			'jetpack-recipes-block-view-styles',
178
			Jetpack::get_file_url_for_environment( '_inc/blocks/view.css', '_inc/blocks/view.css' ),
179
			array(),
180
			JETPACK__VERSION
181
		);
182
183
		wp_register_style(
184
			'jetpack-recipes-block-editor-styles',
185
			Jetpack::get_file_url_for_environment( '_inc/blocks/editor.css', '_inc/blocks/editor.css' ),
186
			array(),
187
			JETPACK__VERSION
188
		);
189
190
		register_block_type(
191
			'a8c/recipes',
192
			array(
193
				'script'        => 'jetpack-recipes-block-view',
194
				'editor_script' => 'jetpack-recipes-block-editor',
195
				'style'         => 'jetpack-recipes-block-view-styles',
196
				'editor_style'  => 'jetpack-recipes-block-editor-styles',
197
			)
198
		);
199
	}
200
201
	/**
202
	 * Our [recipe] shortcode.
203
	 * Prints recipe data styled to look good on *any* theme.
204
	 *
205
	 * @param array  $atts    Array of shortcode attributes.
206
	 * @param string $content Post content.
207
	 *
208
	 * @return string HTML for recipe shortcode.
209
	 */
210
	static function recipe_shortcode( $atts, $content = '' ) {
211
		$atts = shortcode_atts(
212
			array(
213
				'title'       => '', // string.
214
				'servings'    => '', // intval.
215
				'time'        => '', // string.
216
				'difficulty'  => '', // string.
217
				'print'       => '', // string.
218
				'source'      => '', // string.
219
				'sourceurl'   => '', // string.
220
				'image'       => '', // string.
221
				'description' => '', // string.
222
			),
223
			$atts,
224
			'recipe'
225
		);
226
227
		return self::recipe_shortcode_html( $atts, $content );
228
	}
229
230
	/**
231
	 * The recipe output
232
	 *
233
	 * @param array  $atts    Array of shortcode attributes.
234
	 * @param string $content Post content.
235
	 *
236
	 * @return string HTML output
237
	 */
238
	static function recipe_shortcode_html( $atts, $content = '' ) {
239
240
		$html = '<div class="hrecipe jetpack-recipe" itemscope itemtype="https://schema.org/Recipe">';
241
242
		// Print the recipe title if exists.
243
		if ( '' !== $atts['title'] ) {
244
			$html .= '<h3 class="jetpack-recipe-title" itemprop="name">' . esc_html( $atts['title'] ) . '</h3>';
245
		}
246
247
		// Print the recipe meta if exists.
248
		if ( '' !== $atts['servings'] || '' != $atts['time'] || '' != $atts['difficulty'] || '' != $atts['print'] ) {
249
			$html .= '<ul class="jetpack-recipe-meta">';
250
251 View Code Duplication
			if ( '' !== $atts['servings'] ) {
252
				$html .= sprintf(
253
					'<li class="jetpack-recipe-servings" itemprop=""><strong>%1$s: </strong>%2$s</li>',
254
					esc_html_x( 'Servings', 'recipe', 'jetpack' ),
255
					esc_html( $atts['servings'] )
256
				);
257
			}
258
259
			if ( '' !== $atts['time'] ) {
260
				// Get a time that's supported by Schema.org.
261
				$duration = WPCOM_JSON_API_Date::format_duration( $atts['time'] );
262
				// If no duration can be calculated, let's output what the user provided.
263
				if ( empty( $duration ) ) {
264
					$duration = $atts['time'];
265
				}
266
267
				$html .= sprintf(
268
					'<li class="jetpack-recipe-time">
269
					<time itemprop="totalTime" datetime="%3$s"><strong>%1$s: </strong>%2$s</time>
270
					</li>',
271
					esc_html_x( 'Time', 'recipe', 'jetpack' ),
272
					esc_html( $atts['time'] ),
273
					esc_attr( $duration )
274
				);
275
			}
276
277 View Code Duplication
			if ( '' !== $atts['difficulty'] ) {
278
				$html .= sprintf(
279
					'<li class="jetpack-recipe-difficulty"><strong>%1$s: </strong>%2$s</li>',
280
					esc_html_x( 'Difficulty', 'recipe', 'jetpack' ),
281
					esc_html( $atts['difficulty'] )
282
				);
283
			}
284
285
			if ( '' !== $atts['source'] ) {
286
				$html .= sprintf(
287
					'<li class="jetpack-recipe-source"><strong>%1$s: </strong>',
288
					esc_html_x( 'Source', 'recipe', 'jetpack' )
289
				);
290
291
				if ( '' !== $atts['sourceurl'] ) :
292
					// Show the link if we have one.
293
					$html .= sprintf(
294
						'<a href="%2$s">%1$s</a>',
295
						esc_html( $atts['source'] ),
296
						esc_url( $atts['sourceurl'] )
297
					);
298
				else :
299
					// Skip the link.
300
					$html .= sprintf(
301
						'%1$s',
302
						esc_html( $atts['source'] )
303
					);
304
				endif;
305
306
				$html .= '</li>';
307
			}
308
309
			if ( 'false' !== $atts['print'] ) {
310
				$html .= sprintf(
311
					'<li class="jetpack-recipe-print"><a href="#">%1$s</a></li>',
312
					esc_html_x( 'Print', 'recipe', 'jetpack' )
313
				);
314
			}
315
316
			$html .= '</ul>';
317
		} // End if().
318
319
		// Output the image, if we have one.
320
		if ( '' !== $atts['image'] ) {
321
			$html .= sprintf(
322
				'<img class="jetpack-recipe-image" itemprop="image" src="%1$s" />',
323
				esc_url( $atts['image'] )
324
			);
325
		}
326
327
		// Output the description, if we have one.
328
		if ( '' !== $atts['description'] ) {
329
			$html .= sprintf(
330
				'<p class="jetpack-recipe-description" itemprop="description">%1$s</p>',
331
				esc_html( $atts['description'] )
332
			);
333
		}
334
335
		// Print content between codes.
336
		$html .= '<div class="jetpack-recipe-content">' . do_shortcode( $content ) . '</div>';
337
338
		// Close it up.
339
		$html .= '</div>';
340
341
		// If there is a recipe within a recipe, remove the shortcode.
342
		if ( has_shortcode( $html, 'recipe' ) ) {
343
			remove_shortcode( 'recipe' );
344
		}
345
346
		// Sanitize html.
347
		$html = wp_kses_post( $html );
348
349
		// Return the HTML block.
350
		return $html;
351
	}
352
353
	/**
354
	 * Our [recipe-notes] shortcode.
355
	 * Outputs ingredients, styled in a div.
356
	 *
357
	 * @param array  $atts    Array of shortcode attributes.
358
	 * @param string $content Post content.
359
	 *
360
	 * @return string HTML for recipe notes shortcode.
361
	 */
362 View Code Duplication
	static function recipe_notes_shortcode( $atts, $content = '' ) {
363
		$atts = shortcode_atts(
364
			array(
365
				'title' => '', // string.
366
			),
367
			$atts,
368
			'recipe-notes'
369
		);
370
371
		$html = '';
372
373
		// Print a title if one exists.
374
		if ( '' !== $atts['title'] ) {
375
			$html .= '<h4 class="jetpack-recipe-notes-title">' . esc_html( $atts['title'] ) . '</h4>';
376
		}
377
378
		$html .= '<div class="jetpack-recipe-notes">';
379
380
		// Format content using list functionality, if desired.
381
		$html .= self::output_list_content( $content, 'notes' );
382
383
		$html .= '</div>';
384
385
		// Sanitize html.
386
		$html = wp_kses_post( $html );
387
388
		// Return the HTML block.
389
		return $html;
390
	}
391
392
	/**
393
	 * Our [recipe-ingredients] shortcode.
394
	 * Outputs notes, styled in a div.
395
	 *
396
	 * @param array  $atts    Array of shortcode attributes.
397
	 * @param string $content Post content.
398
	 *
399
	 * @return string HTML for recipe ingredients shortcode.
400
	 */
401 View Code Duplication
	static function recipe_ingredients_shortcode( $atts, $content = '' ) {
402
		$atts = shortcode_atts(
403
			array(
404
				'title' => esc_html_x( 'Ingredients', 'recipe', 'jetpack' ), // string.
405
			),
406
			$atts,
407
			'recipe-ingredients'
408
		);
409
410
		$html = '<div class="jetpack-recipe-ingredients">';
411
412
		// Print a title unless the user has opted to exclude it.
413
		if ( 'false' !== $atts['title'] ) {
414
			$html .= '<h4 class="jetpack-recipe-ingredients-title">' . esc_html( $atts['title'] ) . '</h4>';
415
		}
416
417
		// Format content using list functionality.
418
		$html .= self::output_list_content( $content, 'ingredients' );
419
420
		$html .= '</div>';
421
422
		// Sanitize html.
423
		$html = wp_kses_post( $html );
424
425
		// Return the HTML block.
426
		return $html;
427
	}
428
429
	/**
430
	 * Reusable function to check for shortened formatting.
431
	 * Basically, users can create lists with the following shorthand:
432
	 * - item one
433
	 * - item two
434
	 * - item three
435
	 * And we'll magically convert it to a list. This has the added benefit
436
	 * of including itemprops for the recipe schema.
437
	 *
438
	 * @param string $content HTML content.
439
	 * @param string $type    Type of list.
440
	 *
441
	 * @return string content formatted as a list item
442
	 */
443
	static function output_list_content( $content, $type ) {
444
		$html = '';
445
446
		switch ( $type ) {
447
			case 'directions':
448
				$list_item_replacement = '<li class="jetpack-recipe-directions">${1}</li>';
449
				$itemprop              = ' itemprop="recipeInstructions"';
450
				$listtype              = 'ol';
0 ignored issues
show
Unused Code introduced by
$listtype is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
451
				break;
452
			case 'ingredients':
453
				$list_item_replacement = '<li class="jetpack-recipe-ingredient" itemprop="recipeIngredient">${1}</li>';
454
				$itemprop              = '';
455
				$listtype              = 'ul';
0 ignored issues
show
Unused Code introduced by
$listtype is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
456
				break;
457
			default:
458
				$list_item_replacement = '<li class="jetpack-recipe-notes">${1}</li>';
459
				$itemprop              = '';
460
				$listtype              = 'ul';
0 ignored issues
show
Unused Code introduced by
$listtype is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
461
		}
462
463
		// Check to see if the user is trying to use shortened formatting.
464
		if (
465
			strpos( $content, '&#8211;' ) !== false ||
466
			strpos( $content, '&#8212;' ) !== false ||
467
			strpos( $content, '-' ) !== false ||
468
			strpos( $content, '*' ) !== false ||
469
			strpos( $content, '#' ) !== false ||
470
			strpos( $content, '–' ) !== false || // ndash.
471
			strpos( $content, '—' ) !== false || // mdash.
472
			preg_match( '/\d+\.\s/', $content )
473
		) {
474
			// Remove breaks and extra whitespace.
475
			$content = str_replace( "<br />\n", "\n", $content );
476
			$content = trim( $content );
477
478
			$ul_pattern = '/(?:^|\n|\<p\>)+(?:[\-–—]+|\&#8211;|\&#8212;|\*)+\h+(.*)/mi';
479
			$ol_pattern = '/(?:^|\n|\<p\>)+(?:\d+\.|#+)+\h+(.*)/mi';
480
481
			preg_match_all( $ul_pattern, $content, $ul_matches );
482
			preg_match_all( $ol_pattern, $content, $ol_matches );
483
484
			if ( 0 !== count( $ul_matches[0] ) || 0 !== count( $ol_matches[0] ) ) {
485
486
				if ( 0 !== count( $ol_matches[0] ) ) {
487
					$listtype          = 'ol';
488
					$list_item_pattern = $ol_pattern;
489
				} else {
490
					$listtype          = 'ul';
491
					$list_item_pattern = $ul_pattern;
492
				}
493
				$html .= '<' . $listtype . $itemprop . '>';
494
				$html .= preg_replace( $list_item_pattern, $list_item_replacement, $content );
495
				$html .= '</' . $listtype . '>';
496
497
				// Strip out any empty <p> tags and stray </p> tags, because those are just silly.
498
				$empty_p_pattern = '/(<p>)*\s*<\/p>/mi';
499
				$html            = preg_replace( $empty_p_pattern, '', $html );
500
			} else {
501
				$html .= do_shortcode( $content );
502
			}
503
		} else {
504
			$html .= do_shortcode( $content );
505
		}
506
507
		// Return our formatted content.
508
		return $html;
509
	}
510
511
	/**
512
	 * Our [recipe-directions] shortcode.
513
	 * Outputs directions, styled in a div.
514
	 *
515
	 * @param array  $atts    Array of shortcode attributes.
516
	 * @param string $content Post content.
517
	 *
518
	 * @return string HTML for recipe directions shortcode.
519
	 */
520 View Code Duplication
	static function recipe_directions_shortcode( $atts, $content = '' ) {
521
		$atts = shortcode_atts(
522
			array(
523
				'title' => esc_html_x( 'Directions', 'recipe', 'jetpack' ), // string.
524
			),
525
			$atts,
526
			'recipe-directions'
527
		);
528
529
		$html = '<div class="jetpack-recipe-directions">';
530
531
		// Print a title unless the user has specified to exclude it.
532
		if ( 'false' !== $atts['title'] ) {
533
			$html .= '<h4 class="jetpack-recipe-directions-title">' . esc_html( $atts['title'] ) . '</h4>';
534
		}
535
536
		// Format content using list functionality.
537
		$html .= self::output_list_content( $content, 'directions' );
538
539
		$html .= '</div>';
540
541
		// Sanitize html.
542
		$html = wp_kses_post( $html );
543
544
		// Return the HTML block.
545
		return $html;
546
	}
547
548
	/**
549
	 * Use $themecolors array to style the Recipes shortcode
550
	 *
551
	 * @print style block
552
	 * @return string $style
553
	 */
554
	function themecolor_styles() {
555
		global $themecolors;
556
		$style = '';
557
558
		if ( isset( $themecolors ) ) {
559
			$style .= '.jetpack-recipe { border-color: #' . esc_attr( $themecolors['border'] ) . '; }';
560
			$style .= '.jetpack-recipe-title { border-bottom-color: #' . esc_attr( $themecolors['link'] ) . '; }';
561
		}
562
563
		return $style;
564
	}
565
566
}
567
568
new Jetpack_Recipes();
569