| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700 |
- <?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
- use Automattic\Jetpack\Assets;
- /**
- * Embed recipe 'cards' in post, with basic styling and print functionality
- *
- * To Do
- * - defaults settings
- * - basic styles/themecolor styles
- * - validation/sanitization
- * - print styles
- *
- * @package automattic/jetpack
- */
- /**
- * Register and display Recipes in posts.
- */
- class Jetpack_Recipes {
- /**
- * Have scripts and styles been enqueued already.
- *
- * @var bool
- */
- private $scripts_and_style_included = false;
- /**
- * Constructor
- */
- public function __construct() {
- add_action( 'init', array( $this, 'action_init' ) );
- }
- /**
- * Returns KSES tags with Schema-specific attributes.
- *
- * @since 8.0.0
- *
- * @return array Array to be used by KSES.
- */
- private static function kses_tags() {
- $allowedtags = wp_kses_allowed_html( 'post' );
- // Create an array of all the tags we'd like to add the itemprop attribute to.
- $tags = array( 'li', 'ol', 'ul', 'img', 'p', 'h3', 'time', 'span' );
- foreach ( $tags as $tag ) {
- if ( ! isset( $allowedtags[ $tag ] ) ) {
- $allowedtags[ $tag ] = array();
- }
- $allowedtags[ $tag ]['class'] = array();
- $allowedtags[ $tag ]['itemprop'] = array();
- $allowedtags[ $tag ]['datetime'] = array();
- }
- // Allow the handler <a on=""> in AMP.
- $allowedtags['a']['on'] = array();
- // Allow itemscope and itemtype for divs.
- if ( ! isset( $allowedtags['div'] ) ) {
- $allowedtags['div'] = array();
- }
- $allowedtags['div']['class'] = array();
- $allowedtags['div']['itemscope'] = array();
- $allowedtags['div']['itemtype'] = array();
- return $allowedtags;
- }
- /**
- * Register our shortcode and enqueue necessary files.
- */
- public function action_init() {
- // Enqueue styles if [recipe] exists.
- add_action( 'wp_head', array( $this, 'add_scripts' ), 1 );
- // Render [recipe], along with other shortcodes that can be nested within.
- add_shortcode( 'recipe', array( $this, 'recipe_shortcode' ) );
- add_shortcode( 'recipe-notes', array( $this, 'recipe_notes_shortcode' ) );
- add_shortcode( 'recipe-ingredients', array( $this, 'recipe_ingredients_shortcode' ) );
- add_shortcode( 'recipe-directions', array( $this, 'recipe_directions_shortcode' ) );
- add_shortcode( 'recipe-nutrition', array( $this, 'recipe_nutrition_shortcode' ) );
- add_shortcode( 'recipe-image', array( $this, 'recipe_image_shortcode' ) );
- }
- /**
- * Enqueue scripts and styles
- */
- public function add_scripts() {
- if ( empty( $GLOBALS['posts'] ) || ! is_array( $GLOBALS['posts'] ) ) {
- return;
- }
- foreach ( $GLOBALS['posts'] as $p ) {
- if ( has_shortcode( $p->post_content, 'recipe' ) ) {
- $this->scripts_and_style_included = true;
- break;
- }
- }
- if ( ! $this->scripts_and_style_included ) {
- return;
- }
- wp_enqueue_style( 'jetpack-recipes-style', plugins_url( '/css/recipes.css', __FILE__ ), array(), '20130919' );
- wp_style_add_data( 'jetpack-recipes-style', 'rtl', 'replace' );
- // add $themecolors-defined styles.
- wp_add_inline_style( 'jetpack-recipes-style', self::themecolor_styles() );
- if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
- return;
- }
- wp_enqueue_script(
- 'jetpack-recipes-printthis',
- Assets::get_file_url_for_environment( '_inc/build/shortcodes/js/recipes-printthis.min.js', 'modules/shortcodes/js/recipes-printthis.js' ),
- array( 'jquery' ),
- '20170202',
- false
- );
- wp_enqueue_script(
- 'jetpack-recipes-js',
- Assets::get_file_url_for_environment( '_inc/build/shortcodes/js/recipes.min.js', 'modules/shortcodes/js/recipes.js' ),
- array( 'jquery', 'jetpack-recipes-printthis' ),
- '20131230',
- false
- );
- $title_var = wp_title( '|', false, 'right' );
- $rtl = is_rtl() ? '-rtl' : '';
- $print_css_var = plugins_url( "/css/recipes-print{$rtl}.css", __FILE__ );
- wp_localize_script(
- 'jetpack-recipes-js',
- 'jetpack_recipes_vars',
- array(
- 'pageTitle' => $title_var,
- 'loadCSS' => $print_css_var,
- )
- );
- }
- /**
- * Our [recipe] shortcode.
- * Prints recipe data styled to look good on *any* theme.
- *
- * @param array $atts Array of shortcode attributes.
- * @param string $content Post content.
- *
- * @return string HTML for recipe shortcode.
- */
- public static function recipe_shortcode( $atts, $content = '' ) {
- $atts = shortcode_atts(
- array(
- 'title' => '', // string.
- 'servings' => '', // intval.
- 'time' => '', // strtotime-compatible time description.
- 'difficulty' => '', // string.
- 'print' => '', // URL for external print version.
- 'source' => '', // string.
- 'sourceurl' => '', // URL string. Only used if source set.
- 'image' => '', // URL or attachment ID.
- 'description' => '', // string.
- 'cooktime' => '', // strtotime-compatible time description.
- 'preptime' => '', // strtotime-compatible time description.
- 'rating' => '', // string.
- ),
- $atts,
- 'recipe'
- );
- return self::recipe_shortcode_html( $atts, $content );
- }
- /**
- * The recipe output
- *
- * @param array $atts Array of shortcode attributes.
- * @param string $content Post content.
- *
- * @return string HTML output
- */
- private static function recipe_shortcode_html( $atts, $content = '' ) {
- $html = '<div class="hrecipe h-recipe jetpack-recipe" itemscope itemtype="https://schema.org/Recipe">';
- // Print the recipe title if exists.
- if ( '' !== $atts['title'] ) {
- $html .= '<h3 class="p-name jetpack-recipe-title fn" itemprop="name">' . esc_html( $atts['title'] ) . '</h3>';
- }
- // Print the recipe meta if exists.
- if (
- '' !== $atts['servings']
- || '' !== $atts['time']
- || '' !== $atts['difficulty']
- || '' !== $atts['print']
- || '' !== $atts['preptime']
- || '' !== $atts['cooktime']
- || '' !== $atts['rating']
- ) {
- $html .= '<ul class="jetpack-recipe-meta">';
- if ( '' !== $atts['servings'] ) {
- $html .= sprintf(
- '<li class="jetpack-recipe-servings p-yield yield" itemprop="recipeYield"><strong>%1$s: </strong>%2$s</li>',
- esc_html_x( 'Servings', 'recipe', 'jetpack' ),
- esc_html( $atts['servings'] )
- );
- }
- $time_types = array( 'preptime', 'cooktime', 'time' );
- foreach ( $time_types as $time_type ) {
- if ( '' === $atts[ $time_type ] ) {
- continue;
- }
- $html .= self::output_time( $atts[ $time_type ], $time_type );
- }
- if ( '' !== $atts['difficulty'] ) {
- $html .= sprintf(
- '<li class="jetpack-recipe-difficulty"><strong>%1$s: </strong>%2$s</li>',
- esc_html_x( 'Difficulty', 'recipe', 'jetpack' ),
- esc_html( $atts['difficulty'] )
- );
- }
- if ( '' !== $atts['rating'] ) {
- $html .= sprintf(
- '<li class="jetpack-recipe-rating">
- <strong>%1$s: </strong>
- <span itemprop="contentRating">%2$s</span>
- </li>',
- esc_html_x( 'Rating', 'recipe', 'jetpack' ),
- esc_html( $atts['rating'] )
- );
- }
- if ( '' !== $atts['source'] ) {
- $html .= sprintf(
- '<li class="jetpack-recipe-source"><strong>%1$s: </strong>',
- esc_html_x( 'Source', 'recipe', 'jetpack' )
- );
- if ( '' !== $atts['sourceurl'] ) :
- // Show the link if we have one.
- $html .= sprintf(
- '<a href="%2$s">%1$s</a>',
- esc_html( $atts['source'] ),
- esc_url( $atts['sourceurl'] )
- );
- else :
- // Skip the link.
- $html .= sprintf(
- '%1$s',
- esc_html( $atts['source'] )
- );
- endif;
- $html .= '</li>';
- }
- if ( 'false' !== $atts['print'] ) {
- $is_amp = class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request();
- $print_action = $is_amp ? 'on="tap:AMP.print"' : '';
- $print_text = $is_amp ? esc_html__( 'Print page', 'jetpack' ) : esc_html_x( 'Print', 'recipe', 'jetpack' );
- $html .= sprintf(
- '<li class="jetpack-recipe-print"><a href="#" %1$s>%2$s</a></li>',
- $print_action,
- $print_text
- );
- }
- $html .= '</ul>';
- }
- // Output the image if we have one and it's not shown elsewhere.
- if ( '' !== $atts['image'] ) {
- if ( ! has_shortcode( $content, 'recipe-image' ) ) {
- $html .= self::output_image_html( $atts['image'] );
- }
- }
- // Output the description, if we have one.
- if ( '' !== $atts['description'] ) {
- $html .= sprintf(
- '<p class="jetpack-recipe-description" itemprop="description">%1$s</p>',
- esc_html( $atts['description'] )
- );
- }
- // Print content between codes.
- $html .= '<div class="jetpack-recipe-content">' . do_shortcode( $content ) . '</div>';
- // Close it up.
- $html .= '</div>';
- // If there is a recipe within a recipe, remove the shortcode.
- if ( has_shortcode( $html, 'recipe' ) ) {
- remove_shortcode( 'recipe' );
- }
- // Sanitize html.
- $html = wp_kses( $html, self::kses_tags() );
- // Return the HTML block.
- return $html;
- }
- /**
- * Our [recipe-image] shortcode.
- * Controls placement of image in recipe.
- *
- * @param array $atts Array of shortcode attributes.
- *
- * @return string HTML for recipe notes shortcode.
- */
- public static function recipe_image_shortcode( $atts ) {
- $atts = shortcode_atts(
- array(
- 'image' => '', // string.
- 0 => '', // string.
- ),
- $atts,
- 'recipe-image'
- );
- $src = $atts['image'];
- if ( ! empty( $atts[0] ) ) {
- $src = $atts[0];
- }
- return self::output_image_html( $src );
- }
- /**
- * Our [recipe-notes] shortcode.
- * Outputs ingredients, styled in a div.
- *
- * @param array $atts Array of shortcode attributes.
- * @param string $content Post content.
- *
- * @return string HTML for recipe notes shortcode.
- */
- public static function recipe_notes_shortcode( $atts, $content = '' ) {
- $atts = shortcode_atts(
- array(
- 'title' => '', // string.
- ),
- $atts,
- 'recipe-notes'
- );
- $html = '';
- // Print a title if one exists.
- if ( '' !== $atts['title'] ) {
- $html .= '<h4 class="jetpack-recipe-notes-title">' . esc_html( $atts['title'] ) . '</h4>';
- }
- $html .= '<div class="jetpack-recipe-notes">';
- // Format content using list functionality, if desired.
- $html .= self::output_list_content( $content, 'notes' );
- $html .= '</div>';
- // Sanitize html.
- $html = wp_kses( $html, self::kses_tags() );
- // Return the HTML block.
- return $html;
- }
- /**
- * Our [recipe-ingredients] shortcode.
- * Outputs notes, styled in a div.
- *
- * @param array $atts Array of shortcode attributes.
- * @param string $content Post content.
- *
- * @return string HTML for recipe ingredients shortcode.
- */
- public static function recipe_ingredients_shortcode( $atts, $content = '' ) {
- $atts = shortcode_atts(
- array(
- 'title' => esc_html_x( 'Ingredients', 'recipe', 'jetpack' ), // string.
- ),
- $atts,
- 'recipe-ingredients'
- );
- $html = '<div class="jetpack-recipe-ingredients">';
- // Print a title unless the user has opted to exclude it.
- if ( 'false' !== $atts['title'] ) {
- $html .= '<h4 class="jetpack-recipe-ingredients-title">' . esc_html( $atts['title'] ) . '</h4>';
- }
- // Format content using list functionality.
- $html .= self::output_list_content( $content, 'ingredients' );
- $html .= '</div>';
- // Sanitize html.
- $html = wp_kses( $html, self::kses_tags() );
- // Return the HTML block.
- return $html;
- }
- /**
- * Our [recipe-nutrition] shortcode.
- * Outputs notes, styled in a div.
- *
- * @param array $atts Array of shortcode attributes.
- * @param string $content Post content.
- *
- * @return string HTML for recipe nutrition shortcode.
- */
- public static function recipe_nutrition_shortcode( $atts, $content = '' ) {
- $atts = shortcode_atts(
- array(
- 'title' => esc_html_x( 'Nutrition', 'recipe', 'jetpack' ), // string.
- ),
- $atts,
- 'recipe-nutrition'
- );
- $html = '<div class="jetpack-recipe-nutrition p-nutrition nutrition">';
- // Print a title unless the user has opted to exclude it.
- if ( 'false' !== $atts['title'] ) {
- $html .= '<h4 class="jetpack-recipe-nutrition-title">' . esc_html( $atts['title'] ) . '</h4>';
- }
- // Format content using list functionality.
- $html .= self::output_list_content( $content, 'nutrition' );
- $html .= '</div>';
- // Sanitize html.
- $html = wp_kses( $html, self::kses_tags() );
- // Return the HTML block.
- return $html;
- }
- /**
- * Reusable function to check for shortened formatting.
- * Basically, users can create lists with the following shorthand:
- * - item one
- * - item two
- * - item three
- * And we'll magically convert it to a list. This has the added benefit
- * of including itemprops for the recipe schema.
- *
- * @param string $content HTML content.
- * @param string $type Type of list.
- *
- * @return string content formatted as a list item
- */
- private static function output_list_content( $content, $type ) {
- $html = '';
- switch ( $type ) {
- case 'directions':
- $list_item_replacement = '<li class="jetpack-recipe-directions">${1}</li>';
- $itemprop = ' itemprop="recipeInstructions"';
- $listtype = 'ol';
- break;
- case 'ingredients':
- $list_item_replacement = '<li class="jetpack-recipe-ingredient p-ingredient ingredient" itemprop="recipeIngredient">${1}</li>';
- $itemprop = '';
- $listtype = 'ul';
- break;
- case 'nutrition':
- $list_item_replacement = '<li class="jetpack-recipe-nutrition">${1}</li>';
- $itemprop = ' itemprop="nutrition"';
- $listtype = 'ul';
- break;
- case 'nutrition':
- $list_item_replacement = '<li class="jetpack-recipe-nutrition nutrition">${1}</li>';
- $itemprop = ' itemprop="nutrition"';
- $listtype = 'ul';
- break;
- default:
- $list_item_replacement = '<li class="jetpack-recipe-notes">${1}</li>';
- $itemprop = '';
- $listtype = 'ul';
- }
- // Check to see if the user is trying to use shortened formatting.
- if (
- strpos( $content, '–' ) !== false ||
- strpos( $content, '—' ) !== false ||
- strpos( $content, '-' ) !== false ||
- strpos( $content, '*' ) !== false ||
- strpos( $content, '#' ) !== false ||
- strpos( $content, '–' ) !== false || // ndash.
- strpos( $content, '—' ) !== false || // mdash.
- preg_match( '/\d+\.\s/', $content )
- ) {
- // Remove breaks and extra whitespace.
- $content = str_replace( "<br />\n", "\n", $content );
- $content = trim( $content );
- $ul_pattern = '/(?:^|\n|\<p\>)+(?:[\-–—]+|\–|\—|\*)+\h+(.*)/mi';
- $ol_pattern = '/(?:^|\n|\<p\>)+(?:\d+\.|#+)+\h+(.*)/mi';
- preg_match_all( $ul_pattern, $content, $ul_matches );
- preg_match_all( $ol_pattern, $content, $ol_matches );
- if ( 0 !== count( $ul_matches[0] ) || 0 !== count( $ol_matches[0] ) ) {
- if ( 0 !== count( $ol_matches[0] ) ) {
- $listtype = 'ol';
- $list_item_pattern = $ol_pattern;
- } else {
- $listtype = 'ul';
- $list_item_pattern = $ul_pattern;
- }
- $html .= '<' . $listtype . $itemprop . '>';
- $html .= preg_replace( $list_item_pattern, $list_item_replacement, $content );
- $html .= '</' . $listtype . '>';
- // Strip out any empty <p> tags and stray </p> tags, because those are just silly.
- $empty_p_pattern = '/(<p>)*\s*<\/p>/mi';
- $html = preg_replace( $empty_p_pattern, '', $html );
- } else {
- $html .= do_shortcode( $content );
- }
- } else {
- $html .= do_shortcode( $content );
- }
- // Return our formatted content.
- return $html;
- }
- /**
- * Our [recipe-directions] shortcode.
- * Outputs directions, styled in a div.
- *
- * @param array $atts Array of shortcode attributes.
- * @param string $content Post content.
- *
- * @return string HTML for recipe directions shortcode.
- */
- public static function recipe_directions_shortcode( $atts, $content = '' ) {
- $atts = shortcode_atts(
- array(
- 'title' => esc_html_x( 'Directions', 'recipe', 'jetpack' ), // string.
- ),
- $atts,
- 'recipe-directions'
- );
- $html = '<div class="jetpack-recipe-directions e-instructions">';
- // Print a title unless the user has specified to exclude it.
- if ( 'false' !== $atts['title'] ) {
- $html .= '<h4 class="jetpack-recipe-directions-title">' . esc_html( $atts['title'] ) . '</h4>';
- }
- // Format content using list functionality.
- $html .= self::output_list_content( $content, 'directions' );
- $html .= '</div>';
- // Sanitize html.
- $html = wp_kses( $html, self::kses_tags() );
- // Return the HTML block.
- return $html;
- }
- /**
- * Outputs time meta tag.
- *
- * @param string $time_str Raw time to output.
- * @param string $time_type Type of time to show.
- *
- * @return string HTML for recipe time meta.
- */
- private static function output_time( $time_str, $time_type ) {
- // Get a time that's supported by Schema.org.
- $duration = WPCOM_JSON_API_Date::format_duration( $time_str );
- // If no duration can be calculated, let's output what the user provided.
- if ( ! $duration ) {
- $duration = $time_str;
- }
- switch ( $time_type ) {
- case 'cooktime':
- $title = _x( 'Cook Time', 'recipe', 'jetpack' );
- $itemprop = 'cookTime';
- break;
- case 'preptime':
- $title = _x( 'Prep Time', 'recipe', 'jetpack' );
- $itemprop = 'prepTime';
- break;
- default:
- $title = _x( 'Time', 'recipe', 'jetpack' );
- $itemprop = 'totalTime';
- break;
- }
- return sprintf(
- '<li class="jetpack-recipe-%3$s">
- <time itemprop="%4$s" datetime="%5$s"><strong>%1$s:</strong> <span class="%3$s">%2$s</span></time>
- </li>',
- esc_html( $title ),
- esc_html( $time_str ),
- esc_attr( $time_type ),
- esc_attr( $itemprop ),
- esc_attr( $duration )
- );
- }
- /**
- * Outputs image tag for recipe.
- *
- * @param string $src The image source.
- *
- * @return string
- */
- private static function output_image_html( $src ) {
- // Exit if there is no provided source.
- if ( ! $src ) {
- return '';
- }
- $image_attrs = array(
- 'class' => 'jetpack-recipe-image u-photo photo',
- 'itemprop' => 'image',
- );
- if (
- function_exists( 'wp_lazy_loading_enabled' )
- && wp_lazy_loading_enabled( 'img', 'wp_get_attachment_image' )
- ) {
- $image_attrs['loading'] = 'lazy';
- }
- // If it's numeric, this may be an attachment.
- if ( is_numeric( $src ) ) {
- return wp_get_attachment_image(
- $src,
- 'full',
- false,
- $image_attrs
- );
- }
- // Check if it's an absolute or relative URL, and return if not.
- if (
- 0 !== strpos( $src, '/' )
- && false === filter_var( $src, FILTER_VALIDATE_URL )
- ) {
- return '';
- }
- $image_attrs_markup = '';
- foreach ( $image_attrs as $name => $value ) {
- $image_attrs_markup .= sprintf(
- ' %1$s="%2$s"',
- esc_attr( $name ),
- esc_attr( $value )
- );
- }
- return sprintf(
- '<img%1$s src="%2$s" />',
- $image_attrs_markup,
- esc_url( $src )
- );
- }
- /**
- * Use $themecolors array to style the Recipes shortcode
- *
- * @print style block
- * @return string $style
- */
- public function themecolor_styles() {
- global $themecolors;
- $style = '';
- if ( isset( $themecolors ) ) {
- $style .= '.jetpack-recipe { border-color: #' . esc_attr( $themecolors['border'] ) . '; }';
- $style .= '.jetpack-recipe-title { border-bottom-color: #' . esc_attr( $themecolors['link'] ) . '; }';
- }
- return $style;
- }
- }
- new Jetpack_Recipes();
|