Completed
Push — update/recipe-shortcode-wpcom-... ( 56be3a )
by Jeremy
11:45
created

Jetpack_Recipes   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 468
Duplicated Lines 20.94 %

Coupling/Cohesion

Components 2
Dependencies 0

Importance

Changes 0
Metric Value
dl 98
loc 468
rs 7.9487
c 0
b 0
f 0
wmc 52
lcom 2
cbo 0

12 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
B add_recipes_kses_rules() 0 29 3
A add_kses_rule() 0 12 2
A action_init() 0 10 1
C add_scripts() 6 40 7
A recipe_shortcode() 0 17 1
F recipe_shortcode_html() 21 104 15
B recipe_notes_shortcode() 25 25 2
A recipe_ingredients_shortcode() 23 23 2
C output_list_content() 0 67 14
A recipe_directions_shortcode() 23 23 2
A themecolor_styles() 0 11 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Jetpack_Recipes often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Jetpack_Recipes, and based on these observations, apply Extract Interface, too.

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