recipe.php 14 KB

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