and #is',
static function (array $match) use (&$styles): string {
$styles[] = $match[2];
return '';
},
$html
);
$html = (string) preg_replace_callback(
'#)#is',
static function (array $match): string {
self::queueScript(self::parseAttributes($match[1]), isset($match[2]) ? (string) $match[2] : '');
return '';
},
$html
);
$html = (string) preg_replace_callback(
'#]*)>#is',
static function (array $match) use (&$stylesheets): string {
$attributes = self::parseAttributes($match[1]);
$rel = isset($attributes['rel']) ? strtolower($attributes['rel']) : '';
if (strpos($rel, 'stylesheet') !== false && !empty($attributes['href'])) {
$stylesheets[] = $attributes;
}
return '';
},
$html
);
$html = (string) preg_replace(
[
'#
]*>.*?#is',
'##is',
'#]*>#is',
'#]*>#is',
],
'',
$html
);
foreach ($stylesheets as $attributes) {
self::addStylesheet($attributes);
}
if ($styles) {
self::addStyles(implode("\n", $styles));
}
return $html;
}
/**
* Prints queued scripts. Hooked to wp_footer once something was queued.
*/
public static function printFooterScripts(): void
{
$scripts = self::$scripts;
self::$scripts = [];
foreach ($scripts as $script) {
if (isset($script['attributes']['src'])) {
wp_print_script_tag($script['attributes']);
continue;
}
wp_print_inline_script_tag($script['content'], $script['attributes']);
}
}
/**
* Keeps the queued scripts out of the builder (scripts in innerHTML never
* run there) and out of contexts without WordPress' script API.
*
* @param array $attributes
*/
private static function queueScript(array $attributes, string $content): void
{
if (self::isBuilder() || !function_exists('wp_print_inline_script_tag')) {
return;
}
if (trim($content) === '' && !isset($attributes['src'])) {
return;
}
if (isset($attributes['src']) && function_exists('esc_url_raw')) {
$attributes['src'] = esc_url_raw($attributes['src']);
}
self::$scripts[] = ['content' => $content, 'attributes' => $attributes];
if (!self::$footerHooked && function_exists('add_action')) {
self::$footerHooked = true;
add_action('wp_footer', [self::class, 'printFooterScripts'], 30);
}
}
/**
* @param array $attributes
*/
private static function addStylesheet(array $attributes): void
{
if (!function_exists('wp_enqueue_style') || !function_exists('esc_url_raw')) {
return;
}
$href = esc_url_raw($attributes['href']);
if ($href === '') {
return;
}
// A printed by wp_print_styles() carries id="-css":
// reuse that registered handle so WordPress prints it at most once.
$id = isset($attributes['id']) ? (string) $attributes['id'] : '';
if (substr($id, -4) === '-css' && wp_style_is(substr($id, 0, -4), 'registered')) {
$original = substr($id, 0, -4);
if (self::isBuilder()) {
wp_print_styles([$original]);
} else {
wp_enqueue_style($original);
}
return;
}
$handle = self::STYLE_HANDLE . '-' . md5($href);
$media = isset($attributes['media']) && $attributes['media'] !== '' ? $attributes['media'] : 'all';
// Keep the third-party file's own version: move its ver= query
// argument into the version parameter so WordPress adds it back once.
$version = FCBD_VERSION;
$query = (string) wp_parse_url($href, PHP_URL_QUERY);
if ($query !== '') {
parse_str($query, $args);
if (isset($args['ver']) && is_string($args['ver']) && $args['ver'] !== '') {
$version = $args['ver'];
$href = remove_query_arg('ver', $href);
}
}
wp_register_style($handle, $href, [], $version, $media);
if (self::isBuilder()) {
wp_print_styles([$handle]);
return;
}
wp_enqueue_style($handle);
}
private static function addStyles(string $css): void
{
if (!function_exists('wp_register_style') || !function_exists('wp_add_inline_style')) {
return;
}
if (self::isBuilder()) {
$handle = self::STYLE_HANDLE . '-builder-' . (++self::$builderStyleCounter);
wp_register_style($handle, false, [], FCBD_VERSION);
wp_add_inline_style($handle, $css);
wp_print_styles([$handle]);
return;
}
if (!wp_style_is(self::STYLE_HANDLE, 'registered')) {
wp_register_style(self::STYLE_HANDLE, false, [], FCBD_VERSION);
}
wp_add_inline_style(self::STYLE_HANDLE, $css);
wp_enqueue_style(self::STYLE_HANDLE);
}
/**
* @return array
*/
private static function parseAttributes(string $raw): array
{
$attributes = [];
if (!preg_match_all('#([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|([^\s"\'=<>`]+)))?#', $raw, $matches, PREG_SET_ORDER)) {
return $attributes;
}
foreach ($matches as $match) {
$name = strtolower($match[1]);
$value = $match[2] ?? '';
if ($value === '' && isset($match[3]) && $match[3] !== '') {
$value = $match[3];
}
if ($value === '' && isset($match[4]) && $match[4] !== '') {
$value = $match[4];
}
$attributes[$name] = html_entity_decode($value, ENT_QUOTES);
}
return $attributes;
}
private static function isBuilder(): bool
{
return class_exists('FluentCartBreakdance\\Context\\ProductContext')
&& \FluentCartBreakdance\Context\ProductContext::isBuilderRequest();
}
/**
* Tags (and their own attributes) that the 'post' context lacks or
* restricts, as emitted by FluentCart's renderers and this plugin.
*
* @return array>
*/
private static function extraTags(): array
{
$formCommon = [
'name' => true,
'value' => true,
'disabled' => true,
'form' => true,
'autocomplete' => true,
'autofocus' => true,
'required' => true,
'readonly' => true,
];
$svgPresentation = [
'fill' => true,
'fill-rule' => true,
'fill-opacity' => true,
'clip-rule' => true,
'clip-path' => true,
'stroke' => true,
'stroke-width' => true,
'stroke-linecap' => true,
'stroke-linejoin' => true,
'stroke-miterlimit' => true,
'stroke-dasharray' => true,
'stroke-dashoffset' => true,
'stroke-opacity' => true,
'opacity' => true,
'transform' => true,
'color' => true,
'mask' => true,
'filter' => true,
'vector-effect' => true,
'shape-rendering' => true,
];
$svgShape = $svgPresentation + [
'd' => true,
'cx' => true,
'cy' => true,
'r' => true,
'rx' => true,
'ry' => true,
'x' => true,
'y' => true,
'x1' => true,
'y1' => true,
'x2' => true,
'y2' => true,
'width' => true,
'height' => true,
'points' => true,
'pathlength' => true,
];
$filterPrimitive = [
'in' => true,
'in2' => true,
'result' => true,
'x' => true,
'y' => true,
'width' => true,
'height' => true,
'color-interpolation-filters' => true,
];
return [
'form' => [
'action' => true,
'method' => true,
'enctype' => true,
'novalidate' => true,
'name' => true,
'target' => true,
'autocomplete' => true,
'accept-charset' => true,
],
'input' => $formCommon + [
'type' => true,
'checked' => true,
'placeholder' => true,
'min' => true,
'max' => true,
'minlength' => true,
'maxlength' => true,
'step' => true,
'pattern' => true,
'size' => true,
'multiple' => true,
'accept' => true,
'inputmode' => true,
'list' => true,
],
'select' => $formCommon + [
'multiple' => true,
'size' => true,
],
'option' => [
'value' => true,
'selected' => true,
'disabled' => true,
'label' => true,
],
'optgroup' => [
'label' => true,
'disabled' => true,
],
'textarea' => $formCommon + [
'placeholder' => true,
'rows' => true,
'cols' => true,
'minlength' => true,
'maxlength' => true,
'wrap' => true,
],
'label' => [
'for' => true,
],
'button' => $formCommon + [
'type' => true,
'formaction' => true,
'formmethod' => true,
'formnovalidate' => true,
'popovertarget' => true,
],
'fieldset' => [
'disabled' => true,
'name' => true,
'form' => true,
],
'legend' => [],
'datalist' => [],
'output' => [
'for' => true,
'name' => true,
'form' => true,
],
'progress' => [
'value' => true,
'max' => true,
],
'meter' => [
'value' => true,
'min' => true,
'max' => true,
'low' => true,
'high' => true,
'optimum' => true,
],
'img' => [
'srcset' => true,
'sizes' => true,
'loading' => true,
'decoding' => true,
'fetchpriority' => true,
'crossorigin' => true,
'referrerpolicy' => true,
],
'picture' => [],
'source' => [
'src' => true,
'srcset' => true,
'sizes' => true,
'type' => true,
'media' => true,
'width' => true,
'height' => true,
],
'a' => [
'download' => true,
'hreflang' => true,
'type' => true,
'referrerpolicy' => true,
],
'iframe' => [
'src' => true,
'name' => true,
'width' => true,
'height' => true,
'allow' => true,
'allowfullscreen' => true,
'allowtransparency' => true,
'frameborder' => true,
'loading' => true,
'referrerpolicy' => true,
'sandbox' => true,
'scrolling' => true,
'spellcheck' => true,
],
'dialog' => [
'open' => true,
],
'template' => [],
// Mount point of FluentCart's customer dashboard Vue app.
'app' => [],
'slot' => [
'name' => true,
],
'svg' => $svgPresentation + [
'xmlns' => true,
'xmlns:xlink' => true,
'version' => true,
'viewbox' => true,
'viewBox' => true,
'width' => true,
'height' => true,
'x' => true,
'y' => true,
'preserveaspectratio' => true,
'preserveAspectRatio' => true,
'focusable' => true,
'xml:space' => true,
],
'g' => $svgPresentation,
'path' => $svgShape,
'circle' => $svgShape,
'ellipse' => $svgShape,
'rect' => $svgShape,
'line' => $svgShape,
'polyline' => $svgShape,
'polygon' => $svgShape,
'text' => $svgShape + [
'dx' => true,
'dy' => true,
'text-anchor' => true,
'dominant-baseline' => true,
'font-size' => true,
'font-family' => true,
'font-weight' => true,
],
'tspan' => $svgShape + [
'dx' => true,
'dy' => true,
],
'defs' => [],
'symbol' => [
'viewbox' => true,
'viewBox' => true,
'preserveaspectratio' => true,
],
'use' => $svgShape + [
'href' => true,
'xlink:href' => true,
],
'lineargradient' => [
'gradientunits' => true,
'gradienttransform' => true,
'x1' => true,
'y1' => true,
'x2' => true,
'y2' => true,
'href' => true,
'xlink:href' => true,
],
'radialgradient' => [
'gradientunits' => true,
'gradienttransform' => true,
'cx' => true,
'cy' => true,
'r' => true,
'fx' => true,
'fy' => true,
'href' => true,
'xlink:href' => true,
],
'stop' => [
'offset' => true,
'stop-color' => true,
'stop-opacity' => true,
],
'clippath' => [
'clippathunits' => true,
],
'mask' => [
'x' => true,
'y' => true,
'width' => true,
'height' => true,
'maskunits' => true,
],
'pattern' => [
'x' => true,
'y' => true,
'width' => true,
'height' => true,
'patternunits' => true,
],
'desc' => [],
'animate' => [
'attributename' => true,
'attributetype' => true,
'from' => true,
'to' => true,
'values' => true,
'by' => true,
'dur' => true,
'begin' => true,
'end' => true,
'repeatcount' => true,
'repeatdur' => true,
'fill' => true,
'keytimes' => true,
'keysplines' => true,
'calcmode' => true,
],
'animatetransform' => [
'attributename' => true,
'attributetype' => true,
'type' => true,
'from' => true,
'to' => true,
'values' => true,
'by' => true,
'dur' => true,
'begin' => true,
'end' => true,
'repeatcount' => true,
'repeatdur' => true,
'fill' => true,
'additive' => true,
'keytimes' => true,
'calcmode' => true,
],
'filter' => [
'x' => true,
'y' => true,
'width' => true,
'height' => true,
'filterunits' => true,
'primitiveunits' => true,
'color-interpolation-filters' => true,
],
'feflood' => $filterPrimitive + ['flood-color' => true, 'flood-opacity' => true],
'fecolormatrix' => $filterPrimitive + ['type' => true, 'values' => true],
'feoffset' => $filterPrimitive + ['dx' => true, 'dy' => true],
'fegaussianblur' => $filterPrimitive + ['stddeviation' => true, 'edgemode' => true],
'feblend' => $filterPrimitive + ['mode' => true],
'fecomposite' => $filterPrimitive + ['operator' => true, 'k1' => true, 'k2' => true, 'k3' => true, 'k4' => true],
'femerge' => $filterPrimitive,
'femergenode' => ['in' => true],
'fedropshadow' => $filterPrimitive + ['dx' => true, 'dy' => true, 'stddeviation' => true, 'flood-color' => true, 'flood-opacity' => true],
];
}
}