From e92335728460a7a32e4f0f9f84f68352b3340135 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Wed, 8 Apr 2026 12:35:56 +0200 Subject: [PATCH 1/2] Add WooCommerce integration and extensibility filters Exposes WooCommerce products at .md URLs with price, SKU, stock, attributes, and variations. Introduces two filters (markdown_alternate_frontmatter, markdown_alternate_content_sections) plus a cache_version filter so any plugin can extend the markdown output without subclassing. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Filip Ilic --- readme.txt | 35 ++++ src/Integration/WooCommerce.php | 361 ++++++++++++++++++++++++++++++++ src/Output/ContentRenderer.php | 232 +++++++++++++++----- src/Plugin.php | 5 + 4 files changed, 585 insertions(+), 48 deletions(-) create mode 100644 src/Integration/WooCommerce.php diff --git a/readme.txt b/readme.txt index 36b46f1..3c4db19 100644 --- a/readme.txt +++ b/readme.txt @@ -35,6 +35,7 @@ After activation, supported content can be requested in multiple ways: * Caches generated Markdown for better performance * Lets developers enable custom post types * Integrates with Yoast SEO `llms.txt` generation when Yoast SEO is active +* Integrates with WooCommerce — exposes products at `.md` URLs with price, SKU, stock, attributes, and variations **Good to know:** @@ -98,8 +99,42 @@ The output includes converted post content plus front matter such as the title, Yes. When Yoast SEO is active and generating `llms.txt`, Markdown Alternate can rewrite supported post URLs to their `.md` versions. += Does it work with WooCommerce? = + +Yes. When WooCommerce is active, products are automatically served at `.md` URLs. The output includes price, sale price, SKU, stock status, product categories and tags in the frontmatter, plus a summary block, attributes, and variations (for variable products) in the body. + += How can my plugin add custom fields or sections to the markdown output? = + +Two filters let you extend any post's markdown output: + +* `markdown_alternate_frontmatter` — add YAML keys. +* `markdown_alternate_content_sections` — add ordered body sections (lower `priority` runs first; the title is `0`, the post content is `100`). + +Example: + +`add_filter( 'markdown_alternate_frontmatter', function( $data, $post ) { + $data['cook_time'] = get_post_meta( $post->ID, '_cook_time', true ); + return $data; +}, 10, 2 ); + +add_filter( 'markdown_alternate_content_sections', function( $sections, $post ) { + $sections['my_ingredients'] = [ + 'priority' => 120, + 'markdown' => "## Ingredients\n\n- Flour\n- Sugar", + ]; + return $sections; +}, 10, 2 );` + +If your data lives outside `post_content` (e.g. postmeta), call `delete_transient( 'md_alt_cache_' . $post_id )` from your own update hooks so cached output stays fresh. + == Changelog == += 1.2.0 = +* Added WooCommerce integration: products are exposed at `.md` URLs with price, SKU, stock, attributes, and variations +* Added the `markdown_alternate_frontmatter` filter so integrations can add YAML frontmatter keys +* Added the `markdown_alternate_content_sections` filter so integrations can add ordered body sections +* Added the `markdown_alternate_cache_version` filter for integration-driven cache invalidation + = 1.1.0 = * Added transient caching with a 24 hour default and post-modified validation * Added the `markdown_alternate_cache_expiration` filter for cache control diff --git a/src/Integration/WooCommerce.php b/src/Integration/WooCommerce.php new file mode 100644 index 0000000..0cccbc2 --- /dev/null +++ b/src/Integration/WooCommerce.php @@ -0,0 +1,361 @@ +post_type !== 'product' ) { + return $data; + } + + $product = $this->get_product( $post->ID ); + if ( ! $product ) { + return $data; + } + + $sku = $product->get_sku(); + if ( $sku !== '' ) { + $data['sku'] = $sku; + } + + $data['product_type'] = $product->get_type(); + $data['currency'] = function_exists( 'get_woocommerce_currency' ) ? get_woocommerce_currency() : ''; + + $price = $product->get_price(); + $regular = $product->get_regular_price(); + $sale_price = $product->get_sale_price(); + if ( $price !== '' && $price !== null ) { + $data['price'] = (string) $price; + } + if ( $regular !== '' && $regular !== null ) { + $data['regular_price'] = (string) $regular; + } + if ( $sale_price !== '' && $sale_price !== null ) { + $data['sale_price'] = (string) $sale_price; + } + + $data['stock_status'] = $product->get_stock_status(); + if ( $product->managing_stock() ) { + $stock_qty = $product->get_stock_quantity(); + if ( $stock_qty !== null ) { + $data['stock_quantity'] = (int) $stock_qty; + } + } + + $product_categories = $this->collect_product_terms( $post->ID, 'product_cat' ); + if ( $product_categories ) { + $data['product_categories'] = $product_categories; + } + + $product_tags = $this->collect_product_terms( $post->ID, 'product_tag' ); + if ( $product_tags ) { + $data['product_tags'] = $product_tags; + } + + return $data; + } + + /** + * Add product-specific body sections to the markdown output. + * + * @param array $sections Existing sections. + * @param WP_Post $post The post being rendered. + * @return array + */ + public function add_sections( $sections, $post ): array { + if ( ! is_array( $sections ) ) { + $sections = []; + } + if ( ! ( $post instanceof WP_Post ) || $post->post_type !== 'product' ) { + return $sections; + } + + $product = $this->get_product( $post->ID ); + if ( ! $product ) { + return $sections; + } + + $summary = $this->build_summary_section( $product ); + if ( $summary !== '' ) { + $sections['woo_summary'] = [ 'priority' => 10, 'markdown' => $summary ]; + } + + $short_description = trim( (string) $product->get_short_description() ); + if ( $short_description !== '' ) { + $sections['woo_short_description'] = [ + 'priority' => 50, + 'markdown' => "## Summary\n\n" . $this->html_to_text( $short_description ), + ]; + } + + $attributes = $this->build_attributes_section( $product ); + if ( $attributes !== '' ) { + $sections['woo_attributes'] = [ 'priority' => 150, 'markdown' => $attributes ]; + } + + $variations = $this->build_variations_section( $product ); + if ( $variations !== '' ) { + $sections['woo_variations'] = [ 'priority' => 160, 'markdown' => $variations ]; + } + + return $sections; + } + + /** + * Build the at-a-glance summary block (price, stock, SKU). + * + * @param \WC_Product $product The product. + * @return string + */ + private function build_summary_section( $product ): string { + $rows = []; + + $price_html = $product->get_price_html(); + if ( $price_html !== '' ) { + $rows[] = '**Price:** ' . trim( wp_strip_all_tags( $price_html ) ); + } + + $sku = $product->get_sku(); + if ( $sku !== '' ) { + $rows[] = '**SKU:** ' . $sku; + } + + $stock_status = $product->get_stock_status(); + $stock_label = [ + 'instock' => 'In stock', + 'outofstock' => 'Out of stock', + 'onbackorder' => 'On backorder', + ]; + $rows[] = '**Availability:** ' . ( $stock_label[ $stock_status ] ?? $stock_status ); + + if ( ! $rows ) { + return ''; + } + + return implode( "\n", $rows ); + } + + /** + * Build the attributes section. + * + * @param \WC_Product $product The product. + * @return string + */ + private function build_attributes_section( $product ): string { + $attributes = $product->get_attributes(); + if ( ! $attributes ) { + return ''; + } + + $lines = [ '## Attributes', '' ]; + $any = false; + foreach ( $attributes as $attribute ) { + // Skip attributes hidden from product page. + if ( method_exists( $attribute, 'get_visible' ) && ! $attribute->get_visible() ) { + continue; + } + + $label = wc_attribute_label( $attribute->get_name() ); + $values = []; + + if ( $attribute->is_taxonomy() ) { + $terms = wc_get_product_terms( $product->get_id(), $attribute->get_name(), [ 'fields' => 'names' ] ); + if ( $terms ) { + $values = $terms; + } + } else { + $values = $attribute->get_options(); + } + + if ( ! $values ) { + continue; + } + + $lines[] = '- **' . $label . ':** ' . implode( ', ', array_map( 'strval', $values ) ); + $any = true; + } + + return $any ? implode( "\n", $lines ) : ''; + } + + /** + * Build the variations section for variable products. + * + * @param \WC_Product $product The product. + * @return string + */ + private function build_variations_section( $product ): string { + if ( ! $product->is_type( 'variable' ) || ! method_exists( $product, 'get_available_variations' ) ) { + return ''; + } + + $variations = $product->get_available_variations(); + if ( ! $variations ) { + return ''; + } + + $lines = [ '## Variations', '' ]; + foreach ( $variations as $variation ) { + $attrs = []; + if ( ! empty( $variation['attributes'] ) && is_array( $variation['attributes'] ) ) { + foreach ( $variation['attributes'] as $key => $value ) { + if ( $value === '' ) { + continue; + } + $clean_key = ucfirst( str_replace( [ 'attribute_pa_', 'attribute_' ], '', (string) $key ) ); + $attrs[] = $clean_key . ': ' . $value; + } + } + $label = $attrs ? implode( ', ', $attrs ) : ( '#' . ( $variation['variation_id'] ?? '' ) ); + $price = isset( $variation['display_price'] ) ? (string) $variation['display_price'] : ''; + $sku = $variation['sku'] ?? ''; + $detail = []; + if ( $price !== '' ) { + $detail[] = 'price: ' . $price; + } + if ( $sku !== '' ) { + $detail[] = 'SKU: ' . $sku; + } + $lines[] = '- ' . $label . ( $detail ? ' — ' . implode( ', ', $detail ) : '' ); + } + + return implode( "\n", $lines ); + } + + /** + * Collect product taxonomy terms (categories/tags) as name/url pairs. + * + * Mirrors the structure used by core categories/tags so the YAML serializer + * emits them the same way. + * + * @param int $post_id The product ID. + * @param string $taxonomy The taxonomy name. + * @return array + */ + private function collect_product_terms( int $post_id, string $taxonomy ): array { + $terms = get_the_terms( $post_id, $taxonomy ); + if ( ! $terms || is_wp_error( $terms ) ) { + return []; + } + + $out = []; + foreach ( $terms as $term ) { + $url = get_term_link( $term ); + $path = is_wp_error( $url ) ? '' : rtrim( (string) wp_parse_url( $url, PHP_URL_PATH ), '/' ) . '.md'; + $out[] = [ + 'name' => $term->name, + 'url' => $path, + ]; + } + return $out; + } + + /** + * Resolve a `WC_Product` instance for a post id. + * + * @param int $post_id The product ID. + * @return \WC_Product|null + */ + private function get_product( int $post_id ) { + if ( ! function_exists( 'wc_get_product' ) ) { + return null; + } + $product = wc_get_product( $post_id ); + return $product ? $product : null; + } + + /** + * Convert a small chunk of HTML to plain text suitable for embedding in markdown. + * + * @param string $html The HTML. + * @return string + */ + private function html_to_text( string $html ): string { + $text = wp_strip_all_tags( $html ); + $text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); + return trim( $text ); + } + + /** + * Invalidate the cached markdown for a product. + * + * @param int $product_id The product ID. + * @return void + */ + public function invalidate_cache( $product_id ): void { + $product_id = (int) $product_id; + if ( $product_id > 0 ) { + delete_transient( 'md_alt_cache_' . $product_id ); + } + } + + /** + * Invalidate cache when given a product object (variation stock hooks pass the product). + * + * @param \WC_Product $product The product. + * @return void + */ + public function invalidate_cache_from_product( $product ): void { + if ( is_object( $product ) && method_exists( $product, 'get_id' ) ) { + $parent_id = method_exists( $product, 'get_parent_id' ) ? (int) $product->get_parent_id() : 0; + $this->invalidate_cache( $parent_id ?: (int) $product->get_id() ); + } + } +} diff --git a/src/Output/ContentRenderer.php b/src/Output/ContentRenderer.php index 7c740bc..9e72db1 100644 --- a/src/Output/ContentRenderer.php +++ b/src/Output/ContentRenderer.php @@ -17,6 +17,10 @@ * - YAML frontmatter with metadata (title, date, author, categories, tags) * - H1 title heading * - HTML to markdown conversion + * + * Integrations can extend the output through two filters: + * - `markdown_alternate_frontmatter` — add/modify YAML frontmatter keys. + * - `markdown_alternate_content_sections` — add/reorder body sections. */ class ContentRenderer { @@ -46,11 +50,28 @@ public function render(WP_Post $post): string { $transient_key = 'md_alt_cache_' . $post->ID; $cached_data = get_transient( $transient_key ); + /** + * Filters the cache version token mixed into the cache key. + * + * Integrations that derive markdown output from data outside post_content + * (e.g. postmeta) should return a token that changes when their data changes, + * so cached output is invalidated correctly. Alternatively, integrations may + * call delete_transient( 'md_alt_cache_' . $post_id ) on their own update hooks. + * + * @since 1.2.0 + * + * @param string $version The cache version token. Default empty string. + * @param WP_Post $post The post being rendered. + */ + $cache_version = (string) apply_filters( 'markdown_alternate_cache_version', '', $post ); + // Check if cache exists and post hasn't been modified since. - if ( is_array( $cached_data ) && isset( $cached_data['markdown'], $cached_data['modified'] ) ) { - if ( $post->post_modified === $cached_data['modified'] ) { - return $cached_data['markdown']; - } + if ( is_array( $cached_data ) + && isset( $cached_data['markdown'], $cached_data['modified'], $cached_data['version'] ) + && $post->post_modified === $cached_data['modified'] + && $cache_version === $cached_data['version'] + ) { + return $cached_data['markdown']; } $frontmatter = $this->generate_frontmatter($post); @@ -74,9 +95,36 @@ public function render(WP_Post $post): string { ); } - $output = $frontmatter . "\n\n"; - $output .= '# ' . $this->decode_entities($title) . "\n\n"; - $output .= $body; + // Build ordered body sections. Integrations can add their own via the filter. + $default_sections = [ + 'title' => [ + 'priority' => 0, + 'markdown' => '# ' . $this->decode_entities($title), + ], + 'content' => [ + 'priority' => 100, + 'markdown' => $body, + ], + ]; + + /** + * Filters the ordered list of body sections rendered after the frontmatter. + * + * Each section is an array with: + * - 'priority' (int) — sort key, lower runs first. Title is 0, post_content is 100. + * - 'markdown' (string) — the markdown to emit. Empty strings are skipped. + * + * Section keys are arbitrary; use a unique prefix (e.g. "woo_price") to avoid + * collisions with other integrations and to allow targeted removal/replacement. + * + * @since 1.2.0 + * + * @param array $sections Default sections keyed by name. + * @param WP_Post $post The post being rendered. + */ + $sections = apply_filters( 'markdown_alternate_content_sections', $default_sections, $post ); + + $output = $frontmatter . "\n\n" . $this->render_sections( $sections ); // Cache the result (default 24 hours). @@ -91,77 +139,165 @@ public function render(WP_Post $post): string { set_transient( $transient_key, array( 'markdown' => $output, 'modified' => $post->post_modified, + 'version' => $cache_version, ), $expiration ); return $output; } + /** + * Sort sections by priority and concatenate their markdown. + * + * @param array $sections Sections from the markdown_alternate_content_sections filter. + * @return string + */ + private function render_sections(array $sections): string { + // Tolerate malformed entries from third-party filters. + $sections = array_filter( $sections, static function ( $section ) { + return is_array( $section ) + && isset( $section['markdown'] ) + && is_string( $section['markdown'] ) + && trim( $section['markdown'] ) !== ''; + } ); + + uasort( $sections, static function ( $a, $b ) { + $pa = isset( $a['priority'] ) ? (int) $a['priority'] : 100; + $pb = isset( $b['priority'] ) ? (int) $b['priority'] : 100; + return $pa <=> $pb; + } ); + + return implode( "\n\n", array_map( + static fn( $section ) => rtrim( $section['markdown'] ), + $sections + ) ); + } + /** * Generate YAML frontmatter for a post. * + * Builds an associative data array first, runs it through the + * `markdown_alternate_frontmatter` filter so integrations can add keys, + * and then serializes the result to YAML. + * * @param WP_Post $post The post to generate frontmatter for. * @return string The YAML frontmatter block. */ private function generate_frontmatter(WP_Post $post): string { - $lines = ['---']; - - // Title (always included) - $title = get_the_title($post); - $lines[] = 'title: "' . $this->escape_yaml($title) . '"'; - - // Date (always included) - $date = get_the_date('Y-m-d', $post); - $lines[] = 'date: ' . $date; - - // Author (always included) - $author = get_the_author_meta('display_name', $post->post_author); - $lines[] = 'author: "' . $this->escape_yaml($author) . '"'; - - // Featured image (only if set) - $featured_image = get_the_post_thumbnail_url($post->ID, 'full'); - if ($featured_image) { - $lines[] = 'featured_image: "' . $this->escape_yaml($featured_image) . '"'; + $data = [ + 'title' => get_the_title( $post ), + 'date' => get_the_date( 'Y-m-d', $post ), + 'author' => get_the_author_meta( 'display_name', $post->post_author ), + ]; + + $featured_image = get_the_post_thumbnail_url( $post->ID, 'full' ); + if ( $featured_image ) { + $data['featured_image'] = $featured_image; } - // Categories (only if present and not WP_Error) - $category_lines = $this->format_taxonomy_terms('category', $post->ID); - if ($category_lines) { - $lines[] = 'categories:'; - $lines = array_merge($lines, $category_lines); + $categories = $this->collect_taxonomy_terms( 'category', $post->ID ); + if ( $categories ) { + $data['categories'] = $categories; } - // Tags (only if present and not WP_Error) - $tag_lines = $this->format_taxonomy_terms('post_tag', $post->ID); - if ($tag_lines) { - $lines[] = 'tags:'; - $lines = array_merge($lines, $tag_lines); + $tags = $this->collect_taxonomy_terms( 'post_tag', $post->ID ); + if ( $tags ) { + $data['tags'] = $tags; } - $lines[] = '---'; + /** + * Filters the frontmatter data array before it is serialized to YAML. + * + * Integrations can add scalar values, lists, or lists of associative arrays + * (mirroring the shape of `categories` / `tags`). Keys with `null` or empty + * values will be skipped. + * + * @since 1.2.0 + * + * @param array $data Associative array of frontmatter keys and values. + * @param WP_Post $post The post being rendered. + */ + $data = apply_filters( 'markdown_alternate_frontmatter', $data, $post ); - return implode("\n", $lines); + return $this->serialize_frontmatter( $data ); } /** - * Format taxonomy terms as YAML lines. + * Collect taxonomy terms as an array of name + markdown URL pairs. * * @param string $taxonomy The taxonomy name (e.g., 'category', 'post_tag'). - * @param int $post_id The post ID. - * @return array Array of formatted YAML lines, or empty array if no terms. + * @param int $post_id The post ID. + * @return array List of ['name' => ..., 'url' => ...] entries. */ - private function format_taxonomy_terms(string $taxonomy, int $post_id): array { - $terms = get_the_terms($post_id, $taxonomy); - if (!$terms || is_wp_error($terms)) { + private function collect_taxonomy_terms(string $taxonomy, int $post_id): array { + $terms = get_the_terms( $post_id, $taxonomy ); + if ( ! $terms || is_wp_error( $terms ) ) { return []; } - $lines = []; - foreach ($terms as $term) { - $lines[] = ' - name: "' . $this->escape_yaml($term->name) . '"'; - $lines[] = ' url: "' . $this->get_term_markdown_url($term) . '"'; + $out = []; + foreach ( $terms as $term ) { + $out[] = [ + 'name' => $term->name, + 'url' => $this->get_term_markdown_url( $term ), + ]; + } + return $out; + } + + /** + * Serialize a frontmatter data array to a YAML block. + * + * Supports scalars, plain lists of scalars, and lists of associative arrays. + * + * @param array $data The frontmatter data. + * @return string The serialized YAML block (including delimiters). + */ + private function serialize_frontmatter(array $data): string { + $lines = ['---']; + + foreach ( $data as $key => $value ) { + if ( $value === null || $value === '' || $value === [] ) { + continue; + } + + if ( is_array( $value ) ) { + // List of associative arrays (e.g. categories/tags). + if ( isset( $value[0] ) && is_array( $value[0] ) ) { + $lines[] = $key . ':'; + foreach ( $value as $entry ) { + $first = true; + foreach ( $entry as $sub_key => $sub_value ) { + $prefix = $first ? ' - ' : ' '; + $lines[] = $prefix . $sub_key . ': "' . $this->escape_yaml( (string) $sub_value ) . '"'; + $first = false; + } + } + continue; + } + + // Plain list of scalars. + $lines[] = $key . ':'; + foreach ( $value as $scalar ) { + $lines[] = ' - "' . $this->escape_yaml( (string) $scalar ) . '"'; + } + continue; + } + + // Scalars: quote strings, leave plain dates/numbers unquoted when safe. + if ( is_string( $value ) && preg_match( '/^\d{4}-\d{2}-\d{2}$/', $value ) ) { + $lines[] = $key . ': ' . $value; + } elseif ( is_int( $value ) || is_float( $value ) ) { + $lines[] = $key . ': ' . $value; + } elseif ( is_bool( $value ) ) { + $lines[] = $key . ': ' . ( $value ? 'true' : 'false' ); + } else { + $lines[] = $key . ': "' . $this->escape_yaml( (string) $value ) . '"'; + } } - return $lines; + $lines[] = '---'; + + return implode( "\n", $lines ); } /** diff --git a/src/Plugin.php b/src/Plugin.php index 3bbab76..74ce3e2 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -8,6 +8,7 @@ namespace MarkdownAlternate; use MarkdownAlternate\Discovery\AlternateLinkHandler; +use MarkdownAlternate\Integration\WooCommerce; use MarkdownAlternate\Integration\YoastLlmsTxt; use MarkdownAlternate\Router\RewriteHandler; use YahnisElsts\PluginUpdateChecker\v5\PucFactory; @@ -78,6 +79,10 @@ public function register_integrations(): void { if ( defined( 'WPSEO_VERSION' ) ) { ( new YoastLlmsTxt() )->register(); } + + if ( class_exists( 'WooCommerce' ) ) { + ( new WooCommerce() )->register(); + } } /** From 476e95a7adc8f32eaef354047ec65679ad2ea6d8 Mon Sep 17 00:00:00 2001 From: Filip Ilic Date: Wed, 8 Apr 2026 12:48:47 +0200 Subject: [PATCH 2/2] Clean up Woo price formatting in summary block get_price_html() injects screen-reader spans and NBSP entities that look garbled in markdown. Build the price from raw values via wc_price() instead, and decode entities/NBSP for clean output. Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Filip Ilic --- src/Integration/WooCommerce.php | 44 ++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/src/Integration/WooCommerce.php b/src/Integration/WooCommerce.php index 0cccbc2..8983af7 100644 --- a/src/Integration/WooCommerce.php +++ b/src/Integration/WooCommerce.php @@ -167,9 +167,9 @@ public function add_sections( $sections, $post ): array { private function build_summary_section( $product ): string { $rows = []; - $price_html = $product->get_price_html(); - if ( $price_html !== '' ) { - $rows[] = '**Price:** ' . trim( wp_strip_all_tags( $price_html ) ); + $price_line = $this->format_price( $product ); + if ( $price_line !== '' ) { + $rows[] = '**Price:** ' . $price_line; } $sku = $product->get_sku(); @@ -192,6 +192,44 @@ private function build_summary_section( $product ): string { return implode( "\n", $rows ); } + /** + * Format a product price as clean plain text. + * + * Avoids `get_price_html()` because it injects screen-reader spans + * ("Original price was:", "Current price is:") and NBSP entities that + * don't belong in markdown output. + * + * @param \WC_Product $product The product. + * @return string + */ + private function format_price( $product ): string { + if ( ! function_exists( 'wc_price' ) ) { + return ''; + } + + $regular = $product->get_regular_price(); + $sale = $product->get_sale_price(); + $price = $product->get_price(); + + $clean = function ( $html ) { + $text = wp_strip_all_tags( (string) $html ); + $text = html_entity_decode( $text, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); + // Normalize NBSP to a regular space. + $text = str_replace( "\xc2\xa0", ' ', $text ); + return trim( $text ); + }; + + if ( $sale !== '' && $sale !== null && $regular !== '' && $regular !== null && (float) $sale < (float) $regular ) { + return $clean( wc_price( $sale ) ) . ' (was ' . $clean( wc_price( $regular ) ) . ')'; + } + + if ( $price !== '' && $price !== null ) { + return $clean( wc_price( $price ) ); + } + + return ''; + } + /** * Build the attributes section. *