PluginProbe
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler / 1.7.0
FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler v1.7.0
1.7.0 1.6.6 1.6.5 1.6.4 1.6.3 1.6.2 1.6.1 1.6.0 1.5.4 1.5.5 1.5.3 1.5.2 1.5.1 1.5.0 1.4.2 1.4.1 1.4.0 1.3.28 1.3.27 1.3.26 1.3.25 1.3.23 1.3.22 1.3.21 1.3.20 All 50 releases
fluent-cart / app / Services / Renderer / ProductCardRender.php

ProductCardRender.php in FluentCart A New Era of eCommerce – Faster, Lighter, and Simpler 1.7.0, at app/Services/Renderer/ProductCardRender.php

642 lines 26.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentCart\App\Services\Renderer;
4
5 use FluentCart\Api\ModuleSettings;
6 use FluentCart\App\Helpers\Helper;
7 use FluentCart\App\Models\Product;
8 use FluentCart\App\Modules\Templating\AssetLoader;
9 use FluentCart\Framework\Support\Arr;
10
11 class ProductCardRender
12 {
13 protected $product;
14
15 protected $viewUrl = '';
16
17 protected $config = [];
18
19 public function __construct(Product $product, $config = [])
20 {
21 $this->product = $product;
22 $this->viewUrl = $product->view_url;
23 $this->config = $config;
24 }
25
26 public function renderWrapperStart()
27 {
28
29 }
30
31 public function renderWrapperEnd()
32 {
33
34 }
35
36 public function render()
37 {
38 AssetLoader::loadSingleProductAssets();
39 // Lets extensions (e.g. Pro's advanced-variation selector) enqueue the
40 // CSS/JS a product purchase UI needs. Backs the card surfaces that go
41 // through this renderer's render() — Shop, Product List, the product
42 // shortcode, and the quick-view modal. Surfaces that render card parts
43 // via the individual render* methods instead of render() (Product
44 // Carousel, Related Products) fire this same hook from their own block
45 // render. Guarded with a static flag so a grid of N cards fires it once
46 // per request, not once per card. No payload — assets are uniform.
47 static $assetsHookFired = false;
48 if (!$assetsHookFired) {
49 $assetsHookFired = true;
50 do_action('fluent_cart/advanced_variation/enqueue_assets');
51 }
52 $cursor = '';
53 if (!empty($this->config['cursor'])) {
54 $cursor = 'data-fluent-cart-cursor="' . esc_attr($this->config['cursor']) . '"';
55 }
56
57 $cardWidth = '';
58 $cardWidthValue = Arr::get($this->config, 'card_width', '');
59 if ($cardWidthValue) {
60 $widthValue = $cardWidthValue === '100%' ? '100%' : intval($cardWidthValue) . 'px';
61 $cardWidth = 'style="width: ' . esc_attr($widthValue) . ';"';
62 }
63
64
65 // Card wrapper classes are filterable so an integration can tag cards
66 // (sale, low-stock, layout variants) without wrapping or re-rendering
67 // the whole card. Sanitised in RenderGate, so listeners may return a
68 // plain array of class names.
69 $cardClasses = RenderGate::cardClasses(['fct-product-card'], [
70 'product' => $this->product,
71 'scope' => RenderGate::SCOPE_CARD,
72 ]);
73
74 do_action('fluent_cart/product/group/before_card', RenderContext::decorate([
75 'product' => $this->product,
76 'scope' => RenderGate::SCOPE_CARD,
77 ]));
78 ?>
79 <article data-fluent-cart-shop-app-single-product data-fct-product-card=""
80 class="<?php echo esc_attr($cardClasses); ?>"
81 <?php echo $cursor; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already escaped ?>
82 <?php echo $cardWidth; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Already escaped ?>
83 aria-label="<?php echo esc_attr(sprintf(
84 /* translators: %s: product title */
85 __('%s product card', 'fluent-cart'), $this->product->post_title));
86 ?>">
87 <?php $this->renderProductImage(); ?>
88 <?php $this->renderTitle(); ?>
89 <?php $this->renderStarRating(); ?>
90 <?php if (!Arr::get($this->config, 'hide_excerpt', false)) { $this->renderExcerpt(); } ?>
91 <?php $this->renderPrices(); ?>
92 <?php $this->showBuyButton(); ?>
93 </article>
94 <?php
95 do_action('fluent_cart/product/group/after_card', RenderContext::decorate([
96 'product' => $this->product,
97 'scope' => RenderGate::SCOPE_CARD,
98 ]));
99 }
100
101 /**
102 * @deprecated Use PackageDescriptionRenderer::buildPackageInfoFromOtherInfo() directly.
103 * Kept as a compatibility shim for external callers (themes, extensions).
104 *
105 * @param array $otherInfo Order item's other_info array
106 * @return string e.g. "Gift (Box) · 30 × 20 × 15 cm · Wt: 2 kg · Shipping: 2.5 kg"
107 */
108 public static function buildPackageInfoFromOtherInfo($otherInfo)
109 {
110 return PackageDescriptionRenderer::buildPackageInfoFromOtherInfo($otherInfo);
111 }
112
113 /**
114 * @deprecated Use PackageDescriptionRenderer::renderPackageDescription() directly.
115 * Kept as a compatibility shim for external callers (themes, extensions) —
116 * package-description rendering now lives in PackageDescriptionRenderer.
117 */
118 public function renderPackageDescription(
119 $wrapper_attributes = '',
120 $showName = true,
121 $showDimensions = true,
122 $showProductWeight = true,
123 $showTotalWeight = true,
124 $variant = null,
125 $defaultVariant = null
126 ) {
127 (new PackageDescriptionRenderer($this->product))->renderPackageDescription(
128 $wrapper_attributes,
129 $showName,
130 $showDimensions,
131 $showProductWeight,
132 $showTotalWeight,
133 $variant,
134 $defaultVariant
135 );
136 }
137
138 public function renderStarRating()
139 {
140 if (!ModuleSettings::isActive('reviews')) {
141 return;
142 }
143
144 // Store-level toggles: Settings → Store Settings → Product Page →
145 // Product Rating. Shop grid/carousels gate on show_rating_in_shop;
146 // relevant/related product sections gate on show_rating_in_relevant
147 // (callers tag those cards with config rating_context = 'relevant').
148 // The explicit product-rating block (renderStarRatingBlock) stays
149 // unaffected — it is user-placed.
150 $ratingContext = Arr::get($this->config, 'rating_context', 'shop');
151
152 $ratingVisibilitySettingKey = $ratingContext === 'relevant'
153 ? 'show_rating_in_relevant'
154 : 'show_rating_in_shop';
155
156 if ((new \FluentCart\Api\StoreSettings())->get($ratingVisibilitySettingKey, 'yes') !== 'yes') {
157 return;
158 }
159
160 $otherInfo = $this->product->detail->other_info ?: [];
161 $avgRating = Arr::get($otherInfo, 'average_rating', 0);
162 $reviewCount = (int) Arr::get($otherInfo, 'review_count', 0);
163
164 if ($reviewCount < 1) {
165 return;
166 }
167
168 $fullStars = (int) floor($avgRating);
169 $halfStar = ($avgRating - $fullStars) >= 0.5;
170 $emptyStars = 5 - $fullStars - ($halfStar ? 1 : 0);
171
172 ?>
173 <div class="fct-product-card-rating" role="img" aria-label="<?php echo esc_attr(sprintf(
174 /* translators: %1$s: average rating, %2$d: review count */
175 __('Rated %1$s out of 5 based on %2$d reviews', 'fluent-cart'), $avgRating, $reviewCount)); ?>">
176 <span class="fct-product-card-stars" aria-hidden="true">
177 <?php
178 for ($i = 0; $i < $fullStars; $i++) {
179 echo '<span class="fct-star fct-star-filled">&#9733;</span>';
180 }
181 if ($halfStar) {
182 echo '<span class="fct-star fct-star-half"><span class="fct-star-half-empty">&#9733;</span><span class="fct-star-half-fill">&#9733;</span></span>';
183 }
184 for ($i = 0; $i < $emptyStars; $i++) {
185 echo '<span class="fct-star fct-star-empty">&#9733;</span>';
186 }
187 ?>
188 </span>
189 <span class="fct-product-card-review-count">(<?php echo esc_html($reviewCount); ?>)</span>
190 </div>
191 <?php
192 }
193
194 public function renderStarRatingBlock($wrapperAttributes = '')
195 {
196 $otherInfo = $this->product->detail->other_info ?: [];
197 $avgRating = Arr::get($otherInfo, 'average_rating', 0);
198 $reviewCount = (int) Arr::get($otherInfo, 'review_count', 0);
199
200 $fullStars = (int) floor($avgRating);
201 $halfStar = ($avgRating - $fullStars) >= 0.5;
202 $emptyStars = 5 - $fullStars - ($halfStar ? 1 : 0);
203
204 ?>
205 <div <?php echo $wrapperAttributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> role="img" aria-label="<?php echo esc_attr(sprintf(
206 /* translators: %1$s: average rating, %2$d: review count */
207 __('Rated %1$s out of 5 based on %2$d reviews', 'fluent-cart'), $avgRating, $reviewCount)); ?>">
208 <span class="fct-product-card-stars" aria-hidden="true">
209 <?php
210 for ($i = 0; $i < $fullStars; $i++) {
211 echo '<span class="fct-star fct-star-filled">&#9733;</span>';
212 }
213 if ($halfStar) {
214 echo '<span class="fct-star fct-star-half"><span class="fct-star-half-empty">&#9733;</span><span class="fct-star-half-fill">&#9733;</span></span>';
215 }
216 for ($i = 0; $i < $emptyStars; $i++) {
217 echo '<span class="fct-star fct-star-empty">&#9733;</span>';
218 }
219 ?>
220 </span>
221 <span class="fct-product-card-review-count">(<?php echo esc_html($reviewCount); ?>)</span>
222 </div>
223 <?php
224 }
225
226 public function renderExcerpt($atts = '')
227 {
228 if (empty($this->product->post_excerpt)) {
229 return;
230 }
231
232 $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_CARD);
233
234 if (!RenderGate::shouldRender('excerpt', $gateContext)) {
235 return;
236 }
237
238 do_action('fluent_cart/product/group/before_excerpt_block', $gateContext);
239
240 echo sprintf(
241 '<p %1$s class="fct-product-card-excerpt">
242 %2$s
243 </p>',
244 $atts, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
245 wp_kses_post($this->product->post_excerpt),
246 );
247
248 do_action('fluent_cart/product/group/after_excerpt_block', $gateContext);
249 }
250
251 public function renderTitle($atts = '', $config = [])
252 {
253 $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_CARD);
254
255 if (!RenderGate::shouldRender('title', $gateContext)) {
256 return;
257 }
258
259 do_action('fluent_cart/product/group/before_title_block', $gateContext);
260
261 $link = Arr::get($config, 'isLink', true);
262 $target = Arr::get($config, 'target', '_self');
263
264 $titleText = esc_html($this->product->post_title);
265
266 if ($link) {
267 // Render as link
268 $targetAttr = $target === '_blank' ? 'target="_blank" rel="noopener noreferrer"' : '';
269 echo sprintf(
270 '<h3 class="fct-product-card-title">
271 <a %1$s data-fluent-cart-product-link
272 data-product-id="%2$s"
273 href="%3$s"
274 aria-label="%4$s"
275 %5$s>%6$s</a>
276 </h3>',
277 $atts, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
278 esc_attr($this->product->ID),
279 esc_url($this->product->view_url),
280 esc_attr($this->product->post_title),
281 $targetAttr, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
282 esc_html($titleText)
283 );
284 } else {
285 // Render as plain text (no link)
286 echo sprintf(
287 '<h3 class="fct-product-card-title" %1$s>%2$s</h3>',
288 $atts, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
289 esc_html($titleText)
290 );
291 }
292
293 do_action('fluent_cart/product/group/after_title_block', $gateContext);
294 }
295
296 public function renderProductImage()
297 {
298 if (!RenderGate::shouldRender('image', RenderGate::context($this->product, RenderGate::SCOPE_CARD))) {
299 return;
300 }
301
302 // Product::$thumbnail already falls back to the placeholder, which
303 // hides whether this is one, so ask for the featured image directly.
304 $detail = $this->product->detail;
305 $image = $detail ? Arr::get((array) $detail->featured_media, 'url') : '';
306 $isPlaceholder = false;
307
308 if (!$image) {
309 $image = Helper::getProductPlaceholderUrl();
310 $isPlaceholder = true;
311 }
312
313 $altText = $isPlaceholder
314 ? sprintf(
315 /* translators: %s: product title */
316 __('Placeholder image for %s', 'fluent-cart'), $this->product->post_title)
317 : $this->product->post_title;
318
319 do_action('fluent_cart/product/group/before_image_block', RenderContext::decorate([
320 'product' => $this->product,
321 'scope' => 'product_card'
322 ]));
323 ?>
324 <a class="fct-product-card-image-wrap<?php echo $isPlaceholder ? ' is-placeholder' : ''; ?>"
325 href="<?php echo esc_url($this->viewUrl); ?>"
326 style="display: block;"
327 aria-label="<?php echo esc_attr(sprintf(
328 /* translators: %s: product title */
329 __('View %s product image', 'fluent-cart'), $this->product->post_title)); ?>">
330 <img class="fct-product-card-image"
331 data-fluent-cart-shop-app-single-product-image
332 src="<?php echo esc_url($image); ?>"
333 alt="<?php echo esc_attr($altText); ?>"
334 loading="lazy"
335 width="300"
336 height="300"/>
337 </a>
338 <?php
339
340 do_action('fluent_cart/product/group/after_image_block', RenderContext::decorate([
341 'product' => $this->product,
342 'scope' => 'product_card'
343 ]));
344 }
345
346 public function renderPrices($wrapper_attributes = '')
347 {
348 if (!RenderGate::shouldRender('price', RenderGate::context($this->product, RenderGate::SCOPE_CARD))) {
349 return;
350 }
351
352 $priceFormat = Arr::get($this->config, 'price_format', 'starts_from');
353 $isSimple = $this->product->detail->variation_type === 'simple';
354 $minPrice = $this->product->detail->min_price;
355 $maxPrice = $this->product->detail->max_price;
356 $comparePrice = 0;
357 $firstVariant = null;
358
359 if ($isSimple) {
360 $firstVariant = $this->product->variants->first();
361 if ($firstVariant) {
362 $minPrice = $firstVariant->item_price;
363 $minPrice = apply_filters('fluent_cart/product/display_price', $minPrice, [
364 'product' => $this->product,
365 'variation' => $firstVariant,
366 ]);
367 $minPrice = (int)$minPrice;
368 if ($firstVariant->compare_price > $minPrice) {
369 $comparePrice = $firstVariant->compare_price;
370 }
371 }
372 }
373
374 $formattedMinPrice = Helper::toDecimal($minPrice);
375 $formattedMaxPrice = Helper::toDecimal($maxPrice);
376 $formattedComparePrice = Helper::toDecimal($comparePrice);
377
378 do_action('fluent_cart/product/group/before_price_block', RenderContext::decorate([
379 'product' => $this->product,
380 'current_price' => $minPrice,
381 'scope' => 'product_card'
382 ]));
383 ?>
384 <div <?php echo $wrapper_attributes; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
385 class="fct-product-card-prices"
386 role="region"
387 aria-label="<?php echo esc_attr__('Product pricing', 'fluent-cart'); ?>">
388 <?php if ($comparePrice): ?>
389 <span class="fct-compare-price">
390 <span class="fct-sr-only"><?php echo esc_html__('Original price:', 'fluent-cart'); ?></span>
391 <del><?php echo esc_html($formattedComparePrice); ?></del>
392 </span>
393 <?php endif; ?>
394
395 <?php if (!$comparePrice && $maxPrice && $maxPrice > $minPrice): ?>
396 <!-- Case 2: price range -->
397 <?php if ($priceFormat === 'range'): ?>
398 <span class="fct-item-price">
399 <span class="fct-sr-only"><?php echo esc_html__('Price range:', 'fluent-cart'); ?></span>
400 <?php echo esc_html($formattedMinPrice); ?>
401 <span aria-hidden="true">-</span>
402 <span class="fct-sr-only"><?php echo esc_html__('to', 'fluent-cart'); ?></span>
403 <?php echo esc_html($formattedMaxPrice); ?>
404 </span>
405 <?php else: ?>
406 <span class="fct-item-price"><?php
407 /* translators: %s is the minimum price */
408 printf(esc_html__('From %s', 'fluent-cart'), esc_html($formattedMinPrice));
409 ?></span>
410 <?php endif; ?>
411
412 <?php else: ?>
413 <!-- Case 3: Simple or single price -->
414 <span class="fct-item-price">
415 <span class="fct-sr-only"><?php echo esc_html__('Price:', 'fluent-cart'); ?></span>
416 <?php echo esc_html($formattedMinPrice); ?>
417 </span>
418 <?php endif; ?>
419
420 <?php
421 do_action('fluent_cart/product/after_price', RenderContext::decorate([
422 'product' => $this->product,
423 'variant' => $firstVariant,
424 'current_price' => $minPrice,
425 'scope' => 'product_card'
426 ]));
427 RenderHelper::renderPriceSuffix($this->product, $firstVariant, 'product_card');
428 ?>
429 </div>
430 <?php
431 do_action('fluent_cart/product/group/after_price_block', RenderContext::decorate([
432 'product' => $this->product,
433 'current_price' => $minPrice,
434 'scope' => 'product_card'
435 ]));
436 }
437
438 public function showBuyButton($atts = '')
439 {
440 $gateContext = RenderGate::context($this->product, RenderGate::SCOPE_CARD);
441
442 // 'actions' gates the whole affordance slot, including the disabled
443 // out-of-stock placeholder — hiding the row should not leave a stray
444 // "Not Available" button behind.
445 if (!RenderGate::shouldRender('actions', $gateContext)) {
446 return;
447 }
448
449 do_action('fluent_cart/product/group/before_actions_block', $gateContext);
450
451 $this->renderBuyButtonMarkup($atts, $gateContext);
452
453 do_action('fluent_cart/product/group/after_actions_block', $gateContext);
454 }
455
456 protected function renderBuyButtonMarkup($atts, array $gateContext)
457 {
458 $isOutOfStock = ModuleSettings::isActive('stock_management') && !$this->product->isStock();
459
460 if ($isOutOfStock) {
461 $soldOutText = __('Not Available', 'fluent-cart');
462 ?>
463 <button type="button" class="fct-product-view-button out-of-stock" disabled aria-disabled="true"
464 aria-label="<?php echo esc_attr($soldOutText); ?>">
465 <span class="fct-button-text">
466 <?php echo esc_html($soldOutText); ?>
467 </span>
468 </button>
469 <?php
470 return;
471 }
472
473 $enableModalCheckout = Helper::isModalCheckoutEnabled();
474 $isSimple = $this->product->detail->variation_type === 'simple';
475 $firstVariant = null;
476 $buttonHref = $this->viewUrl;
477
478 if ($isSimple) {
479 $firstVariant = $this->product->variants->first();
480 if ($firstVariant) {
481 // return '';
482 }
483 }
484
485 $isInstantCheckout = false;
486 $hasSubscription = $this->product->has_subscription;
487 $buttonText = __('View Options', 'fluent-cart');
488 $ariaLabel = sprintf(
489 /* translators: %s: product title */
490 __('View options for %s', 'fluent-cart'), $this->product->post_title);
491
492 if ($isSimple) {
493 if ($hasSubscription) {
494 $buttonText = __('Buy Now', 'fluent-cart');
495 $ariaLabel = sprintf(
496 /* translators: %s: product title */
497 __('Buy %s now', 'fluent-cart'), $this->product->post_title);
498 $buttonHref = $firstVariant->getPurchaseUrl();
499 $isInstantCheckout = true;
500 } else {
501 $buttonText = __('Add To Cart', 'fluent-cart');
502 $ariaLabel = sprintf(
503 /* translators: %s: product title */
504 __('Add %s to cart', 'fluent-cart'), $this->product->post_title);
505 }
506 }
507
508 // The card fills this slot with one of three things: an instant-checkout
509 // Buy Now anchor (simple + subscription), an Add to Cart button (simple),
510 // or a "View Options" link through to the product page (variable). The
511 // first two are purchase affordances and answer to their own gates.
512 // "View Options" is navigation, so it stays under 'actions' alone — a
513 // catalog-mode listener hides the buying, not the browsing.
514 if ($isInstantCheckout) {
515 if (!RenderGate::shouldRender('buy_now_button', $gateContext)) {
516 return;
517 }
518 } elseif ($firstVariant) {
519 if (!RenderGate::shouldRender('add_to_cart_button', $gateContext)) {
520 return;
521 }
522 }
523
524 $buttonAttributes = [
525 'class' => 'fct-product-view-button fct-single-product-card-view-button',
526 'data-product-id' => $this->product->ID,
527 'data-fluent-cart-single-product-card-view-button' => '',
528 'aria-label' => $ariaLabel
529 ];
530
531 if ($firstVariant) {
532 $buttonAttributes = [
533 'data-cart-id' => $firstVariant->id,
534 'class' => 'fluent-cart-add-to-cart-button',
535 'data-variation-type' => $this->product->detail->variation_type,
536 'data-fluent-cart-add-to-cart-button' => '',
537 'data-is-custom' => false,
538 'aria-label' => $ariaLabel
539 ];
540 }
541 $customAttributes = $this->parseAttributes($atts);
542 if (!empty($customAttributes)) {
543 if (isset($customAttributes['class']) && isset($buttonAttributes['class'])) {
544 $buttonAttributes['class'] .= ' ' . $customAttributes['class'];
545 unset($customAttributes['class']);
546 }
547 $buttonAttributes = array_merge($buttonAttributes, $customAttributes);
548 }
549
550 $anchorAttributes = [
551 'href' => $buttonHref,
552 'class' => 'fct-product-view-button',
553 'aria-label' => $ariaLabel,
554 ];
555
556 if ($enableModalCheckout) {
557 $anchorAttributes['data-fct-instant-checkout-button'] = '';
558 $anchorAttributes['data-enable-modal-checkout'] = 'yes';
559 }
560
561 if ($isInstantCheckout) {
562 $parsedCustomAttributes = $this->parseAttributes($atts);
563 if (isset($parsedCustomAttributes['class']) && isset($anchorAttributes['class'])) {
564 $anchorAttributes['class'] .= ' ' . $parsedCustomAttributes['class'];
565 unset($parsedCustomAttributes['class']);
566 }
567 $anchorAttributes = array_merge($anchorAttributes, $parsedCustomAttributes);
568 }
569 ?>
570 <?php if ($isInstantCheckout): ?>
571 <a <?php $this->renderAttributes($anchorAttributes); ?>>
572 <span aria-hidden="true">
573 <?php echo esc_html($buttonText); ?>
574 </span>
575 </a>
576 <?php else: ?>
577 <button
578 type="button"
579 data-button-url="<?php echo esc_url($buttonHref); ?>"
580 <?php $this->renderAttributes($buttonAttributes); ?>>
581 <span class="fct-button-text">
582 <?php echo esc_html($buttonText); ?>
583 </span>
584 <span
585 class="fluent-cart-loader"
586 role="status"
587 aria-live="polite"
588 aria-label="<?php echo esc_attr__('Loading', 'fluent-cart'); ?>">
589 <svg aria-hidden="true"
590 viewBox="0 0 100 101"
591 fill="none"
592 xmlns="http://www.w3.org/2000/svg"
593 focusable="false">
594 <path
595 d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
596 fill="currentColor"></path>
597 <path
598 d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
599 fill="currentFill"></path>
600 </svg>
601 </span>
602 </button>
603 <?php endif; ?>
604 <?php
605 }
606
607 protected function renderAttributes($atts = [])
608 {
609 foreach ($atts as $attr => $value) {
610 if ($value !== '') {
611 echo esc_attr($attr) . '="' . esc_attr((string)$value) . '" ';
612 } else {
613 echo esc_attr($attr) . ' ';
614 }
615 }
616 }
617
618 private function parseAttributes($atts)
619 {
620 if (empty($atts)) {
621 return [];
622 }
623
624 $attributes = [];
625
626 // Match attribute="value" or attribute='value' or attribute=value
627 preg_match_all('/(\w+(?:-\w+)*)=(["\'])(.*?)\2|\b(\w+(?:-\w+)*)=(\S+)/', $atts, $matches, PREG_SET_ORDER);
628
629 foreach ($matches as $match) {
630 if (!empty($match[1])) {
631 // Quoted value
632 $attributes[$match[1]] = $match[3];
633 } elseif (!empty($match[4])) {
634 // Unquoted value
635 $attributes[$match[4]] = $match[5];
636 }
637 }
638
639 return $attributes;
640 }
641 }
642