From d11a1e631b0aaa6b3841d0fe8e85efbb5938b192 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 11 Jul 2025 15:36:21 -0400 Subject: [PATCH] Updates and new features to the Markup Regions system --- wire/core/WireMarkupRegions.php | 262 ++++++++++++++++++++++++++------ 1 file changed, 217 insertions(+), 45 deletions(-) diff --git a/wire/core/WireMarkupRegions.php b/wire/core/WireMarkupRegions.php index bcfc4de1..1fb2dc76 100644 --- a/wire/core/WireMarkupRegions.php +++ b/wire/core/WireMarkupRegions.php @@ -5,7 +5,7 @@ * * Supports finding and manipulating of markup regions in an HTML document. * - * ProcessWire 3.x, Copyright 2023 by Ryan Cramer + * ProcessWire 3.x, Copyright 2025 by Ryan Cramer * https://processwire.com * */ @@ -37,23 +37,40 @@ class WireMarkupRegions extends Wire { * */ protected $selfClosingTags = array( - 'link', - 'area', - 'base', - 'br', - 'col', - 'command', - 'embed', - 'hr', - 'img', - 'input', - 'keygen', - 'link', - 'meta', - 'param', - 'source', - 'track', - 'wbr', + 'link' => 'link', + 'area' => 'area', + 'base' => 'base', + 'br' => 'br', + 'col' => 'col', + 'command' => 'command', + 'embed' => 'embed', + 'hr' => 'hr', + 'img' => 'img', + 'input' => 'input', + 'keygen' => 'keygen', + 'link' => 'link', + 'meta' => 'meta', + 'param' => 'param', + 'source' => 'source', + 'track' => 'track', + 'wbr' => 'wbr', + ); + + /** + * Tags that generally only appear once in the output + * + * These can be used as unnamed markup regions + * + * @var string[] + * + */ + protected $singles = array( + 'html' => 'html', + 'head' => 'head', + 'title' => 'title', + 'body' => 'body', + 'main' => 'main', + 'base' => 'base', ); /** @@ -63,14 +80,24 @@ class WireMarkupRegions extends Wire { * */ protected $actions = array( - 'prepend', - 'append', - 'before', - 'after', - 'replace', - 'remove', + 'prepend' => 'prepend', + 'append' => 'append', + 'before' => 'before', + 'after' => 'after', + 'replace' => 'replace', + 'remove' => 'remove', + 'update' => 'update', ); + /** + * Markup snippets that should be removed from final output + * + * @var array + * @since 3.0.250 + * + */ + protected $removals = array(); + /** * Locate and return all regions of markup having the given attribute * @@ -737,7 +764,7 @@ class WireMarkupRegions extends Wire { if($name && !isset($attrs[$name])) $attrs[$name] = $val; $tag = rtrim($tag); // remove extra space we added $tagName = strtolower($tagName); - $selfClosing = in_array($tagName, $this->selfClosingTags); + $selfClosing = isset($this->selfClosingTags[$tagName]); $classes = isset($attrs['class']) ? explode(' ', $attrs['class']) : array(); $id = isset($attrs['id']) ? $attrs['id'] : ''; $pwid = ''; @@ -768,7 +795,7 @@ class WireMarkupRegions extends Wire { } else { $actionTarget = $value; } - if($actionTarget && in_array($action, $this->actions)) { + if($actionTarget && isset($this->actions[$action])) { // found a valid action and target unset($attrs[$name]); $actionType = $actionTarget === true ? 'bool' : 'attr'; @@ -786,7 +813,7 @@ class WireMarkupRegions extends Wire { list($prefix, $action) = explode('-', $class, 2); if(strpos($action, '-')) list($action, $actionTarget) = explode('-', $action, 2); if($prefix && $actionTarget) {} // ignore - if(in_array($action, $this->actions)) { + if(isset($this->actions[$action])) { // valid action, remove action from classes and class attribute unset($classes[$key]); $attrs['class'] = implode(' ', $classes); @@ -804,6 +831,10 @@ class WireMarkupRegions extends Wire { // if there's an action, but no target, the target is assumed to be the pw-id or id if($action && (!$actionTarget || $actionTarget === true)) $actionTarget = $pwid; + if(strpos($actionTarget, '^') === 0) { + $actionType = 'tag'; + } + $info = array( 'id' => $id, 'pwid' => $pwid ? $pwid : $id, @@ -1074,6 +1105,17 @@ class WireMarkupRegions extends Wire { $pos = null; + if($name === 'tag') { + if(strpos($value, '.')) { + list($tag, $class) = explode('.', $value, 2); + if(stripos($html, "<$tag") === false) return false; + $value = $class; + $name = 'class'; + } else { + return stripos($html, "<$value>") || stripos($html, "<$value "); + } + } + if($value === true) { $tests = array( " $name ", @@ -1119,16 +1161,21 @@ class WireMarkupRegions extends Wire { if($name == 'id') { $names = '(id|pw-id|data-pw-id)'; } else { - $names = preg_quote($name); + $names = preg_quote($name, '!'); } if($value === true) { + // match only the presence of the attribute $regex = '!<[^<>]*\s' . $names . '[=\s/>]!i'; + } else if($name === 'class') { + // match class even if other class names are present + $regex = '!<[^<>]*\sclass\s*=\s*["\'][^"\'<>]*\b' . preg_quote($value) . '[\s"\']!i'; } else { - $regex = '/<[^<>]*\s' . $names . '\s*=\s*["\']?' . preg_quote($value) . '(?:["\']|[\s>])/i'; + // match attribute value + $regex = '!<[^<>]*\s' . $names . '\s*=\s*["\']?' . preg_quote($value) . '(?:["\']|[\s>])!i'; } if(preg_match($regex, $html)) $pos = true; } - + return $pos !== false; } @@ -1165,6 +1212,13 @@ class WireMarkupRegions extends Wire { if(self::debug) { $findOptions['debugNote'] = "update.$options[action]($selector)"; } + + // convert to tag matching format for find() method + if(strpos($selector, '^') === 0) { + $selector = ltrim($selector, '^'); + // tag is implied if in 'tag.class' format, so only add brackets if no class + if(!strpos($selector, '.')) $selector = "<$selector>"; + } $findRegions = $this->find($selector, $markup, $findOptions); @@ -1179,10 +1233,11 @@ class WireMarkupRegions extends Wire { if($action == 'auto') { // auto mode delegates to the region action $action = ''; - if(in_array($region['action'], $this->actions)) $action = $region['action']; + if(isset($this->actions[$region['action']])) $action = $region['action']; } switch($action) { + case 'update': case 'append': $replacement = $region['open'] . $region['region'] . $content . $region['close']; break; @@ -1297,7 +1352,30 @@ class WireMarkupRegions extends Wire { $options['action'] = 'after'; // after intended return $this->replace($selector, '', $markup, $options); } - + + /** + * Initialize given HTML for markup regions + * + * @param string $html + * @since 3.0.250 + * + */ + protected function initHtml(&$html) { + $tests = [ '="<', "='<", '="<', "='<" ]; + foreach($tests as $test) { + $apply = strpos($html, $test); + if($apply) break; + } + if($apply) { + $actions = implode('|', $this->actions); + $html = preg_replace( + '!(<[^<>]+\s(?:data-pw-|pw-)(?:' . $actions . ')=["\'])(?:<|<)([^<>\'"&]+)(?:>|>)(["\'])!i', + '$1^$2$3', + $html + ); + } + } + /** * Identify and populate markup regions in given HTML * @@ -1379,16 +1457,20 @@ class WireMarkupRegions extends Wire { $defaults = array( 'useClassActions' => false // allow use of "pw-*" class actions? (legacy) ); + + if(is_string($htmlRegions) && $recursionLevel === 1) { + $this->initHtml($htmlRegions); + } $options = array_merge($defaults, $options); $leftoverMarkup = ''; $hasDebugLandmark = strpos($htmlDocument, self::debugLandmark) !== false; $debug = $hasDebugLandmark && $this->wire()->config->debug; $debugTimer = $debug ? Debug::timer() : 0; + $this->populateSingles($htmlDocument, $htmlRegions); if(is_array($htmlRegions)) { $regions = $htmlRegions; - $leftoverMarkup = ''; } else if($this->hasRegions($htmlRegions)) { $htmlRegions = $this->stripRegions('