diff --git a/includes/class.cooked-recipes.php b/includes/class.cooked-recipes.php
index e3841c9..8a0ca45 100644
--- a/includes/class.cooked-recipes.php
+++ b/includes/class.cooked-recipes.php
@@ -1124,11 +1124,12 @@ public static function single_direction($dir, $number = false, $plain_text = fal
echo '<' . $element . ' class="cooked-single-direction cooked-heading">' . esc_html($dir['section_heading_name']) . '' . $element . '>';
}
- } elseif (isset($dir['content']) && $dir['content'] || isset($dir['image']) && $dir['image']) {
+ } elseif (isset($dir['content']) && $dir['content'] || isset($dir['image']) && $dir['image'] || isset($dir['video']) && $dir['video']) {
$dir_image_size = apply_filters( 'cooked_direction_image_size', 'large' );
$image = isset($dir['image']) && $dir['image'] ? wp_get_attachment_image( $dir['image'], $dir_image_size, false, ['title' => esc_attr(get_the_title($dir['image']))] ) : '';
$content = !empty($dir['content']) ? Cooked_Recipes::format_content($dir['content']) : '';
+ $video = isset($dir['video']) && $dir['video'] ? wp_get_attachment_url($dir['video']) : '';
$image = apply_filters('cooked_direction_image_html', $image, $atts);
@@ -1140,7 +1141,7 @@ public static function single_direction($dir, $number = false, $plain_text = fal
echo '
';
echo $number ? '
' . esc_html($number) . ' ' : '';
- echo '
' . do_shortcode($content) . ($image ? wpautop($image) : '') . '
';
+ echo '
' . do_shortcode($content) . ($image ? wpautop($image) : '') . ($video ? ' ' : '') . '
';
echo '
';
}
}
diff --git a/phpcs.xml b/phpcs.xml
index 50c6dc3..12fad7d 100644
--- a/phpcs.xml
+++ b/phpcs.xml
@@ -55,6 +55,7 @@
-->
+
@@ -86,6 +87,10 @@
*.php
+
+ includes/
+
+
@@ -106,5 +111,4 @@
-
-
\ No newline at end of file
+
From d5cf454b65f99e4a68fe48b25fde33714033449f Mon Sep 17 00:00:00 2001
From: mgiannopoulos24 <79588074+mgiannopoulos24@users.noreply.github.com>
Date: Sun, 21 Jun 2026 19:42:54 +0300
Subject: [PATCH 02/18] Update readme and test for new feature.
---
readme.txt | 7 ++++++-
tests/phpunit/RecipeMetaTest.php | 20 ++++++++++++++++++++
2 files changed, 26 insertions(+), 1 deletion(-)
diff --git a/readme.txt b/readme.txt
index a946a4d..853b2f2 100644
--- a/readme.txt
+++ b/readme.txt
@@ -85,11 +85,16 @@ Version 1.15.0 adds metric/imperial measurement system switcher, allergen suppor
== Changelog ==
+= 1.16.0 =
+* **NEW:** Added video clips to recipe direction steps. Attach a video to any direction step using the WordPress Media Library, with square preview thumbnails in the admin and 16:9 responsive video output on the frontend.
+* **TWEAK:** Responsive admin layout improvements.
+* **TWEAK:** Increased direction textarea height to 250px. Meta box title changed from "Settings" to "Cooked Settings".
+
= 1.15.0 =
* **NEW:** Metric/Imperial measurement system switcher. Convert ingredient units between systems with a new toggle alongside the servings switcher. Option available in settings to enable/disable the switcher.
* **NEW:** Added allergen support. Mark up to 14 major allergens (FDA + EU) on a recipe via a new sidebar meta box. Allergen icons are shown on recipe cards (option available in settings to enable/disable) and can be added to single recipes via `[cooked-info include="allergens"]`.
* **TWEAK:** Added [cooked-recipe-card] documentation in the recipe "Shortcodes" tab.
-* **FIX:** Fix to prevent website from crashing if [cooked-recipe id="3587"] shortcode is used in recipe template with the same ID as the recipe.
+* **FIX:** Fix to prevent website from crashing if `[cooked-recipe id="3587"]` shortcode is used in recipe template with the same ID as the recipe.
* **NEW:** Add a wrapping class to recipe output from the [cooked-recipe] shortcode for easier styling.
* **FIX:** Fixed issue with measurement units not translating in nutrient facts.
* **FIX:** Fixed invalid recipe schema when servings were left empty, which caused critical errors in Google Search Console. Servings now default to 1 when not set.
diff --git a/tests/phpunit/RecipeMetaTest.php b/tests/phpunit/RecipeMetaTest.php
index 2d46a85..c87cc0e 100644
--- a/tests/phpunit/RecipeMetaTest.php
+++ b/tests/phpunit/RecipeMetaTest.php
@@ -45,4 +45,24 @@ public function test_meta_cleanup_processes_deeply_nested_arrays() {
$result = Cooked_Recipe_Meta::meta_cleanup($input);
$this->assertArrayHasKey('ingredients', $result);
}
+
+ public function test_meta_cleanup_preserves_direction_video_field() {
+ $input = [
+ 'directions' => [
+ ['content' => 'Mix ingredients', 'video' => '42'],
+ ]
+ ];
+ $result = Cooked_Recipe_Meta::meta_cleanup($input);
+ $this->assertSame('42', $result['directions'][0]['video']);
+ }
+
+ public function test_meta_cleanup_sanitizes_direction_video_field() {
+ $input = [
+ 'directions' => [
+ ['content' => 'Mix', 'video' => ''],
+ ]
+ ];
+ $result = Cooked_Recipe_Meta::meta_cleanup($input);
+ $this->assertStringNotContainsString('',
+ imgOnerror: '
',
+ svgOnload: '
',
+ iframePayload: '',
+ eventHandler: 'click me
',
+ bodyOnload: '',
+ encodedScript: '<script>window.xssTriggered=true;alert("XSS")</script>',
+ nestedScript: '</script>',
+};
+
+// Create a fixture for authentication
+const test = base.extend({
+ adminContext: async ({ browser }, use) => {
+ const context = await browser.newContext({
+ storageState: getAuthPath('mtresova')
+ });
+ const page = await context.newPage();
+ await ensureValidAuth(page, 'mtresova', 'password');
+ await use(context);
+ await page.close();
+ await context.close();
+ }
+});
+
+test.describe('XSS Prevention Tests', () => {
+ // These tests verify that XSS payloads are properly sanitized on save or escaped on output
+
+ test('Recipe title should be escaped against XSS on output', async ({ adminContext }) => {
+ const adminPage = await adminContext.newPage();
+
+ // Track if XSS is triggered via alert dialogs
+ let xssAlertTriggered = false;
+ adminPage.on('dialog', async (dialog) => {
+ if (dialog.message().includes('XSS')) {
+ xssAlertTriggered = true;
+ }
+ await dialog.dismiss();
+ });
+
+ await adminPage.goto('/wp-admin/post-new.php?post_type=cp_recipe', { waitUntil: 'networkidle' });
+
+ const xssTitle = 'Test XSS ' + XSS_PAYLOADS.scriptTag + ' ' + Date.now();
+
+ // Set recipe title with XSS payload
+ await adminPage.getByLabel('Recipe title ...').fill(xssTitle);
+
+ // Set minimum required fields
+ await adminPage.fill('input[name="_recipe_settings[prep_time]"]', '10');
+
+ // Publish the recipe
+ await adminPage.getByRole('button', { name: 'Publish', exact: true }).click();
+
+ await Promise.all([
+ adminPage.waitForURL(/post\.php\?post=\d+&action=edit/),
+ adminPage.waitForSelector('.notice-success', { timeout: 10000 })
+ ]);
+
+ // Get the post ID from the URL
+ const url = adminPage.url();
+ const postIdMatch = url.match(/post=(\d+)/);
+ postId = postIdMatch ? parseInt(postIdMatch[1]) : 0;
+ expect(postId).toBeTruthy();
+
+ // View the recipe on the frontend
+ const frontendPage = await adminContext.newPage();
+
+ // Track XSS on frontend
+ let frontendXssTriggered = false;
+ frontendPage.on('dialog', async (dialog) => {
+ if (dialog.message().includes('XSS')) {
+ frontendXssTriggered = true;
+ }
+ await dialog.dismiss();
+ });
+
+ await frontendPage.goto('/?post_type=cp_recipe&p=' + postId, { waitUntil: 'networkidle' });
+
+ // Check that the XSS was not triggered via window variable or dialogs
+ const xssTriggeredOnPage = await frontendPage.evaluate(() => window.xssTriggered === true);
+
+ expect(xssAlertTriggered).toBe(false);
+ expect(frontendXssTriggered).toBe(false);
+ expect(xssTriggeredOnPage).toBe(false);
+
+ // Cleanup
+ await frontendPage.close();
+
+ // Delete the recipe
+ execSync(`wp post delete ${postId} --force`);
+ });
+
+ test('Recipe excerpt should be sanitized against XSS', async ({ adminContext }) => {
+ const adminPage = await adminContext.newPage();
+
+ let xssAlertTriggered = false;
+ adminPage.on('dialog', async (dialog) => {
+ if (dialog.message().includes('XSS')) {
+ xssAlertTriggered = true;
+ }
+ await dialog.dismiss();
+ });
+
+ await adminPage.goto('/wp-admin/post-new.php?post_type=cp_recipe', { waitUntil: 'networkidle' });
+
+ const title = 'Test Recipe XSS Excerpt: ' + Date.now();
+
+ // Set recipe title
+ await adminPage.getByLabel('Recipe title ...').fill(title);
+ await adminPage.fill('input[name="_recipe_settings[prep_time]"]', '10');
+
+ // Set excerpt with XSS payloads via TinyMCE
+ await adminPage.evaluate((payload) => {
+ const excerptEditor = window.tinyMCE.get('_recipe_settings_excerpt');
+ if (excerptEditor) {
+ excerptEditor.setContent(payload);
+ }
+ }, XSS_PAYLOADS.imgOnerror);
+
+ // Publish the recipe
+ await adminPage.getByRole('button', { name: 'Publish', exact: true }).click();
+
+ await Promise.all([
+ adminPage.waitForURL(/post\.php\?post=\d+&action=edit/),
+ adminPage.waitForSelector('.notice-success', { timeout: 10000 })
+ ]);
+
+ const url = adminPage.url();
+ const postIdMatch = url.match(/post=(\d+)/);
+ postId = postIdMatch ? parseInt(postIdMatch[1]) : 0;
+ expect(postId).toBeTruthy();
+
+ // View the recipe on the frontend
+ const frontendPage = await adminContext.newPage();
+
+ let frontendXssTriggered = false;
+ frontendPage.on('dialog', async (dialog) => {
+ if (dialog.message().includes('XSS')) {
+ frontendXssTriggered = true;
+ }
+ await dialog.dismiss();
+ });
+
+ await frontendPage.goto('/?post_type=cp_recipe&p=' + postId, { waitUntil: 'networkidle' });
+
+ const xssTriggeredOnPage = await frontendPage.evaluate(() => window.xssTriggered === true);
+
+ expect(xssAlertTriggered).toBe(false);
+ expect(frontendXssTriggered).toBe(false);
+ expect(xssTriggeredOnPage).toBe(false);
+
+ // Verify onerror handler is not present
+ const pageContent = await frontendPage.content();
+ expect(pageContent).not.toContain('onerror="window.xssTriggered');
+ expect(pageContent).not.toContain("onerror='window.xssTriggered");
+
+ await frontendPage.close();
+ execSync(`wp post delete ${postId} --force`);
+ });
+
+ test('Recipe notes should be sanitized against XSS', async ({ adminContext }) => {
+ const adminPage = await adminContext.newPage();
+
+ let xssAlertTriggered = false;
+ adminPage.on('dialog', async (dialog) => {
+ if (dialog.message().includes('XSS')) {
+ xssAlertTriggered = true;
+ }
+ await dialog.dismiss();
+ });
+
+ await adminPage.goto('/wp-admin/post-new.php?post_type=cp_recipe', { waitUntil: 'networkidle' });
+
+ const title = 'Test Recipe XSS Notes: ' + Date.now();
+
+ // Set recipe title
+ await adminPage.getByLabel('Recipe title ...').fill(title);
+ await adminPage.fill('input[name="_recipe_settings[prep_time]"]', '10');
+
+ // Set notes with SVG XSS payload via TinyMCE
+ await adminPage.evaluate((payload) => {
+ const notesEditor = window.tinyMCE.get('_recipe_settings_notes');
+ if (notesEditor) {
+ notesEditor.setContent(payload);
+ }
+ }, XSS_PAYLOADS.svgOnload);
+
+ // Publish the recipe
+ await adminPage.getByRole('button', { name: 'Publish', exact: true }).click();
+
+ await Promise.all([
+ adminPage.waitForURL(/post\.php\?post=\d+&action=edit/),
+ adminPage.waitForSelector('.notice-success', { timeout: 10000 })
+ ]);
+
+ const url = adminPage.url();
+ const postIdMatch = url.match(/post=(\d+)/);
+ postId = postIdMatch ? parseInt(postIdMatch[1]) : 0;
+ expect(postId).toBeTruthy();
+
+ // View the recipe on the frontend
+ const frontendPage = await adminContext.newPage();
+
+ let frontendXssTriggered = false;
+ frontendPage.on('dialog', async (dialog) => {
+ if (dialog.message().includes('XSS')) {
+ frontendXssTriggered = true;
+ }
+ await dialog.dismiss();
+ });
+
+ await frontendPage.goto('/?post_type=cp_recipe&p=' + postId, { waitUntil: 'networkidle' });
+
+ const xssTriggeredOnPage = await frontendPage.evaluate(() => window.xssTriggered === true);
+
+ expect(xssAlertTriggered).toBe(false);
+ expect(frontendXssTriggered).toBe(false);
+ expect(xssTriggeredOnPage).toBe(false);
+
+ // Verify onload handler is not present
+ const pageContent = await frontendPage.content();
+ expect(pageContent).not.toContain('onload="window.xssTriggered');
+ expect(pageContent).not.toContain("onload='window.xssTriggered");
+
+ await frontendPage.close();
+ execSync(`wp post delete ${postId} --force`);
+ });
+
+ test('Recipe directions should be sanitized against XSS', async ({ adminContext }) => {
+ const adminPage = await adminContext.newPage();
+
+ let xssAlertTriggered = false;
+ adminPage.on('dialog', async (dialog) => {
+ if (dialog.message().includes('XSS')) {
+ xssAlertTriggered = true;
+ }
+ await dialog.dismiss();
+ });
+
+ await adminPage.goto('/wp-admin/post-new.php?post_type=cp_recipe', { waitUntil: 'networkidle' });
+
+ const title = 'Test Recipe XSS Directions: ' + Date.now();
+
+ // Set recipe title
+ await adminPage.getByLabel('Recipe title ...').fill(title);
+ await adminPage.fill('input[name="_recipe_settings[prep_time]"]', '10');
+
+ // Set direction content with XSS payload
+ await adminPage.evaluate((payload) => {
+ const directionEditors = Object.keys(window.tinyMCE.editors).filter(id => /^direction-\d+-content$/.test(id));
+ if (directionEditors.length > 0) {
+ const directionEditor = window.tinyMCE.get(directionEditors[0]);
+ if (directionEditor) {
+ directionEditor.setContent(payload);
+ }
+ }
+ }, XSS_PAYLOADS.eventHandler);
+
+ // Publish the recipe
+ await adminPage.getByRole('button', { name: 'Publish', exact: true }).click();
+
+ await Promise.all([
+ adminPage.waitForURL(/post\.php\?post=\d+&action=edit/),
+ adminPage.waitForSelector('.notice-success', { timeout: 10000 })
+ ]);
+
+ const url = adminPage.url();
+ const postIdMatch = url.match(/post=(\d+)/);
+ postId = postIdMatch ? parseInt(postIdMatch[1]) : 0;
+ expect(postId).toBeTruthy();
+
+ // View the recipe on the frontend
+ const frontendPage = await adminContext.newPage();
+
+ let frontendXssTriggered = false;
+ frontendPage.on('dialog', async (dialog) => {
+ if (dialog.message().includes('XSS')) {
+ frontendXssTriggered = true;
+ }
+ await dialog.dismiss();
+ });
+
+ await frontendPage.goto('/?post_type=cp_recipe&p=' + postId, { waitUntil: 'networkidle' });
+
+ const xssTriggeredOnPage = await frontendPage.evaluate(() => window.xssTriggered === true);
+
+ expect(xssAlertTriggered).toBe(false);
+ expect(frontendXssTriggered).toBe(false);
+ expect(xssTriggeredOnPage).toBe(false);
+
+ // Verify onclick handler is not present
+ const pageContent = await frontendPage.content();
+ expect(pageContent).not.toContain('onclick="window.xssTriggered');
+ expect(pageContent).not.toContain("onclick='window.xssTriggered");
+
+ await frontendPage.close();
+ execSync(`wp post delete ${postId} --force`);
+ });
+
+ test('Recipe ingredient names should be sanitized against XSS', async ({ adminContext }) => {
+ const adminPage = await adminContext.newPage();
+
+ let xssAlertTriggered = false;
+ adminPage.on('dialog', async (dialog) => {
+ if (dialog.message().includes('XSS')) {
+ xssAlertTriggered = true;
+ }
+ await dialog.dismiss();
+ });
+
+ await adminPage.goto('/wp-admin/post-new.php?post_type=cp_recipe', { waitUntil: 'networkidle' });
+
+ const title = 'Test Recipe XSS Ingredients: ' + Date.now();
+
+ // Set recipe title
+ await adminPage.getByLabel('Recipe title ...').fill(title);
+ await adminPage.fill('input[name="_recipe_settings[prep_time]"]', '10');
+
+ // Set ingredient with XSS payload
+ await adminPage.evaluate((payload) => {
+ const ingredientBlocks = document.querySelectorAll('.cooked-ingredient-block');
+ if (ingredientBlocks.length > 0) {
+ const firstIngredient = ingredientBlocks[0];
+ const amountInput = firstIngredient.querySelector('input[data-ingredient-part="amount"]') as HTMLInputElement;
+ const itemInput = firstIngredient.querySelector('input[data-ingredient-part="name"]') as HTMLInputElement;
+
+ if (amountInput) amountInput.value = '1';
+ if (itemInput) itemInput.value = payload;
+ }
+ }, XSS_PAYLOADS.scriptTag);
+
+ // Publish the recipe
+ await adminPage.getByRole('button', { name: 'Publish', exact: true }).click();
+
+ await Promise.all([
+ adminPage.waitForURL(/post\.php\?post=\d+&action=edit/),
+ adminPage.waitForSelector('.notice-success', { timeout: 10000 })
+ ]);
+
+ const url = adminPage.url();
+ const postIdMatch = url.match(/post=(\d+)/);
+ postId = postIdMatch ? parseInt(postIdMatch[1]) : 0;
+ expect(postId).toBeTruthy();
+
+ // View the recipe on the frontend
+ const frontendPage = await adminContext.newPage();
+
+ let frontendXssTriggered = false;
+ frontendPage.on('dialog', async (dialog) => {
+ if (dialog.message().includes('XSS')) {
+ frontendXssTriggered = true;
+ }
+ await dialog.dismiss();
+ });
+
+ await frontendPage.goto('/?post_type=cp_recipe&p=' + postId, { waitUntil: 'networkidle' });
+
+ const xssTriggeredOnPage = await frontendPage.evaluate(() => window.xssTriggered === true);
+
+ expect(xssAlertTriggered).toBe(false);
+ expect(frontendXssTriggered).toBe(false);
+ expect(xssTriggeredOnPage).toBe(false);
+
+ // Verify the script tag is not present as executable
+ const pageContent = await frontendPage.content();
+ expect(pageContent).not.toContain('';
+ echo ' ';
+ }
+
+ public static function hex2rgb( $hex ) {
+ list( $r,$g,$b ) = sscanf( $hex, "#%02x%02x%02x" );
+ $rgb_array = [$r, $g, $b];
+ return implode( ',',$rgb_array );
+ }
+
+ public static function parse_readme_changelog( $readme_url = false, $title = false ) {
+ ob_start();
+ include !$readme_url ? COOKED_DIR . 'readme.txt' : $readme_url;
+ $readme = ob_get_clean();
+
+ $readme = make_clickable(esc_html($readme));
+ $readme = preg_replace('/`(.*?)`/', '*)+/","",$readme);
+ $readme = explode( $whats_new_title, $readme );
+ $readme = $whats_new_title . $readme[1];
+
+ return $readme;
+ }
+
+ public static function print_options() {
+ $default_print_options = apply_filters( 'cooked_default_print_options', [
+ 'print_options_title' => 'checked',
+ 'print_options_info' => '',
+ 'print_options_excerpt' => '',
+ 'print_options_images' => '',
+ 'print_options_ingredients' => 'checked',
+ 'print_options_directions' => 'checked',
+ 'print_options_notes' => 'checked',
+ 'print_options_nutrition' => '',
+ ]);
+
+ echo '';
+
+ echo '' . __( 'Print','cooked') . ' ';
+ echo '
' . __( 'Print Options:','cooked') . ' ';
+
+ echo ' ' . __('Title','cooked') . ' ';
+ echo ' ' . __('Information','cooked') . ' ';
+ echo ' ' . __('Excerpt','cooked') . ' ';
+ echo ' ' . __('Images','cooked') . ' ';
+ echo ' ' . __('Ingredients','cooked') . ' ';
+ echo ' ' . __('Directions','cooked') . ' ';
+ echo ' ' . __('Notes','cooked') . ' ';
+ echo ' ' . __('Nutrition','cooked') . ' ';
+
+ echo '';
+ }
+
+ public static function print_options_js() {
+ ?>' . "\n";
- }
-
- public function print_view_body_class( $classes ) {
- if ( ! self::is_print_view() ) {
- return $classes;
- }
-
+ public static function print_logo() {
global $_cooked_settings;
- $classes[] = 'cooked-print-view';
-
$print_view_options = isset( $_cooked_settings['print_view_display_options'] ) && is_array( $_cooked_settings['print_view_display_options'] )
? $_cooked_settings['print_view_display_options']
: [];
- if ( ! in_array( 'site_header', $print_view_options, true ) ) {
- $classes[] = 'cooked-print-hide-header';
- }
-
- if ( ! in_array( 'site_footer', $print_view_options, true ) ) {
- $classes[] = 'cooked-print-hide-footer';
- }
-
- return apply_filters( 'cooked_print_body_classes', $classes );
- }
-
- public static function print_view_hide_css() {
- $header_selectors = apply_filters(
- 'cooked_print_header_hide_selectors',
- [
- 'header',
- '#masthead',
- '.site-header',
- '#header',
- '.header',
- '.wp-block-template-part-site-header',
- ]
- );
-
- $footer_selectors = apply_filters(
- 'cooked_print_footer_hide_selectors',
- [
- 'footer',
- '#colophon',
- '.site-footer',
- '#footer',
- '.footer',
- '.wp-block-template-part-site-footer',
- ]
- );
-
- $css = '';
-
- if ( ! empty( $header_selectors ) ) {
- $header_rules = array_map(
- function ( $selector ) {
- return 'body.cooked-print-hide-header ' . trim( $selector );
- },
- $header_selectors
- );
- $css .= implode( ",\n", $header_rules ) . " { display:none !important; }\n";
+ if ( ! in_array( 'site_logo', $print_view_options, true ) ) {
+ return;
}
- if ( ! empty( $footer_selectors ) ) {
- $footer_rules = array_map(
- function ( $selector ) {
- return 'body.cooked-print-hide-footer ' . trim( $selector );
- },
- $footer_selectors
+ echo '';
+
+ $logo_id = get_theme_mod( 'custom_logo' );
+ if ( $logo_id ) {
+ echo wp_get_attachment_image(
+ $logo_id,
+ 'full',
+ false,
+ [
+ 'alt' => get_bloginfo( 'name' ),
+ 'class' => 'cooked-print-logo-image',
+ ]
);
- $css .= implode( ",\n", $footer_rules ) . " { display:none !important; }\n";
+ } else {
+ echo '' . esc_html( get_bloginfo( 'name' ) ) . ' ';
}
- return $css;
+ echo '
';
}
public static function vendor_checks( $content ) {
diff --git a/includes/class.cooked-settings.php b/includes/class.cooked-settings.php
index d60fca4..9d1250d 100644
--- a/includes/class.cooked-settings.php
+++ b/includes/class.cooked-settings.php
@@ -333,6 +333,18 @@ public static function tabs_fields() {
]
)
],
+ 'print_view_display_options' => [
+ 'title' => __('Print View', 'cooked'),
+ 'desc' => __('When enabled, the website logo will appear at the top of the recipe print screen.', 'cooked'),
+ 'type' => 'checkboxes',
+ 'default' => [],
+ 'options' => apply_filters(
+ 'cooked_print_view_display_options',
+ [
+ 'site_logo' => __('Show Website Logo', 'cooked'),
+ ]
+ )
+ ],
'carb_format' => [
'title' => __('Carbs Format', 'cooked'),
'desc' => __('You can display carbs as "Total" or "Net".', 'cooked'),
diff --git a/templates/front/recipe-print.php b/templates/front/recipe-print.php
index afc82c1..1918362 100644
--- a/templates/front/recipe-print.php
+++ b/templates/front/recipe-print.php
@@ -2,10 +2,10 @@
// Print Screen Enqueues
add_action( 'wp_enqueue_scripts', 'cooked_print_enqueues', 99, 1 );
-function cooked_print_enqueues(){
+function cooked_print_enqueues() {
$min = COOKED_DEV ? '' : '.min';
- wp_enqueue_style( 'cooked-print', COOKED_URL . 'assets/css/print' . esc_attr( $min ) . '.css', [], COOKED_VERSION, 'screen,print' );
- wp_enqueue_style( 'cooked-icons', COOKED_URL . 'assets/css/icons' . esc_attr( $min ) . '.css', [], COOKED_VERSION, 'screen,print' );
+ wp_enqueue_style( 'cooked-print', COOKED_URL . 'assets/css/print' . $min . '.css', [], COOKED_VERSION, 'screen,print' );
+ wp_enqueue_style( 'cooked-icons', COOKED_URL . 'assets/css/icons' . $min . '.css', [], COOKED_VERSION, 'screen,print' );
wp_dequeue_style( 'cooked-essentials' );
wp_dequeue_style( 'cooked-styling' );
}
@@ -20,6 +20,10 @@ function cooked_print_enqueues(){
$recipe_settings = Cooked_Recipes::get_settings( $post->ID );
+do_action( 'cooked_before_print_recipe' );
+
+Cooked_Recipes::print_logo();
+
Cooked_Functions::print_options();
echo '' . esc_html( get_the_title() ) . ' ';
@@ -27,7 +31,9 @@ function cooked_print_enqueues(){
Cooked_Functions::print_options_js();
+do_action( 'cooked_after_print_recipe' );
+
wp_footer();
?>
-