From 062ebd6663321a4e2b7055cf1be1942a4df9fcf4 Mon Sep 17 00:00:00 2001 From: Ryan Cramer Date: Fri, 13 Jan 2017 12:23:16 -0500 Subject: [PATCH] Move some code from PageRender.module to WireMarkupRegions.php and make additional updates to WireMarkupRegions, adding support for nested/recursive regions. --- wire/core/WireMarkupRegions.php | 248 +++++++++++++++++++++++++++++++- wire/modules/PageRender.module | 233 +++--------------------------- 2 files changed, 269 insertions(+), 212 deletions(-) diff --git a/wire/core/WireMarkupRegions.php b/wire/core/WireMarkupRegions.php index a24774a4..9f6e0f16 100644 --- a/wire/core/WireMarkupRegions.php +++ b/wire/core/WireMarkupRegions.php @@ -37,7 +37,7 @@ class WireMarkupRegions extends Wire { * */ public function find($selector, $markup, array $options = array()) { - + if(strpos($selector, ',')) return $this->findMulti($selector, $markup, $options); $defaults = array( @@ -886,5 +886,251 @@ class WireMarkupRegions extends Wire { $options['mode'] = 'after'; return $this->replace($selector, '', $markup, $options); } + + /** + * Identify and populate markup regions in given HTML + * + * To use this, you must set `$config->useMarkupRegions = true;` in your /site/config.php file. + * In the future it may be enabled by default for any templates with text/html content-type. + * + * This takes anything output before the opening `` that comes after it. For instance, if there's a `
` in the + * document, then a #content element output prior to the doctype will replace it during page render. + * This enables one to use delayed output as if it’s direct output. It also makes every HTML element + * in the output with an “id” attribute a region that can be populated from any template file. It’s + * a good pairing with a `$config->appendTemplateFile` that contains the main markup and region + * definitions, though can be used with or without it. + * + * Beyond replacement of elements, append, prepend, insert before, insert after, and remove are also + * supported via “pw-” prefix classes that you can add. The classes do not appear in the final output + * markup. When performing replacements or modifications to elements, PW will merge the attributes + * so that attributes present in the final output are present, plus any that were added by the markup + * regions. See the examples for more details. + * + * Below are some examples. Note that “main” is used as an example “id” attribute of an element that + * appears in the main document markup, and the examples below focus on manipulating it. The examples + * assume there is a `
` in the _main.php file (appendTemplateFile), and the lines in the + * examples would be output from a template file, which manipulates what would ultimately be output + * when the page is rendered. + * ~~~~~~ + * Replacing and removing elements + *
This replaces the #main div and merges any attributes
+ *
This does the same as above
+ *
This removes #main completely
+ * + * Prepending and appending elements + *

This prepends #main with this p tag

+ *

This prepends #main and adds "bar" class to main

+ *

This appends #main with this p tag

+ *

This appends #main and adds a "foo" class to #main

+ *
Appends #main with this text + adds title attribute to #main
+ *
Appends #main with this text + removes class “baz” from #main
+ * + * Inserting new elements + *

This adds an h2 headline with this text before #main

+ *

This adds a footer element with this text after #main

+ *
This appends a div.foo to #main with this text
+ *
This prepends a div.bar to #main with this text
+ * ~~~~~~ + * + * @param string $htmlDocument Document to populate regions to + * @param string|array $htmlRegions Markup containing regions (or regions array from a find call) + * @return int Number of updates made to $htmlDocument + * + */ + public function populate(&$htmlDocument, $htmlRegions) { + + static $recursionLevel = 0; + $recursionLevel++; + + if(strpos($htmlDocument, ' id=') === false) return 0; + if(is_array($htmlRegions)) { + $regions = $htmlRegions; + $leftoverMarkup = ''; + } else { + if(!$this->hasRegions($htmlRegions)) return 0; + $regions = $this->find(".pw-*, id=", $htmlRegions, array( + 'verbose' => true, + 'leftover' => true + )); + $leftoverMarkup = trim($regions["leftover"]); + unset($regions["leftover"]); + } + + if(!count($regions)) return 0; + + $xregions = array(); // regions that weren't populated + $populatedNotes = array(); + $rejectedNotes = array(); + $numUpdates = 0; + $debug = $this->wire('config')->debug && $this->wire('user')->isSuperuser(); + + foreach($regions as $regionKey => $region) { + + $xregion = $region; + $this->populateRegionDetails($region); + $mode = $region['mode']; + $id = $region['attrs']['id']; + $regionHTML = $region['region']; + $mergeAttr = $region['attrs']; + $regionNote = "$regionKey. $region[note]"; + unset($mergeAttr['id']); + + if($region['new']) { + // element is newly added element not already present + $mergeAttr = array(); + $regionHTML = $region['html']; + $attrs = $region['attrs']; + unset($attrs['id']); + if(isset($attrs['data-id'])) { + $attrs['id'] = $attrs['data-id']; + unset($attrs['data-id']); + } + $attrStr = count($attrs) ? ' ' . $this->renderAttributes($attrs, false) : ''; + if(!strlen(trim($attrStr))) $attrStr = ''; + $regionHTML = str_replace($region['open'], "<$region[name]$attrStr>", $regionHTML); + } + + // if the id attribute doesn't appear in the html, skip it + if(!$this->hasAttribute('id', $id, $htmlDocument)) { + $xregions[$regionKey] = $xregion; + $rejectedNotes[] = $regionNote; + + } else { + // update the markup + $htmlDocument = $this->update("#$id", $regionHTML, $htmlDocument, array( + 'mode' => $mode, + 'mergeAttr' => $mergeAttr, + )); + + $populatedNotes[] = $regionNote; + $numUpdates++; + } + } + + if($debug) { + $bull = "\n "; + $n = $recursionLevel; + $leftoverBytes = strlen($leftoverMarkup); + $debugNotes = "\nPW markup regions run #$n"; + if(count($populatedNotes)) $debugNotes .= "\n Populated: $bull" . implode($bull, $populatedNotes); + if(count($rejectedNotes)) $debugNotes .= "\n Skipped: $bull" . implode($bull, $rejectedNotes); + if($leftoverBytes) $debugNotes .= "\n $leftoverBytes non-region bytes skipped"; + $landmark = ""; + if(strpos($htmlDocument, $landmark) !== false) { + $debugNotes = "
" . $this->wire('sanitizer')->entities($debugNotes) . "
" . $landmark; + $htmlDocument = str_replace($landmark, $debugNotes, $htmlDocument); + } else { + $htmlDocument .= ""; + } + } + + if(count($xregions) && $recursionLevel < 3) { + // see if they can be populated now + $numUpdates += $this->populate($htmlDocument, $xregions); + } + + // if there is any leftover markup, place it above the HTML where it would usually go + if(strlen($leftoverMarkup)) { + $htmlDocument = $leftoverMarkup . $htmlDocument; + $numUpdates++; + } + + return $numUpdates; + } + + /** + * Determine the and populate 'mode' and 'new' properties for the given region + * + * Modifications are made directly to the given $region. + * + * The 'mode' property can be any of the following values: + * before, after, prepend, append, replace, remove. + * + * The 'new' property will be boolean true if the region is a new element that + * should be added to the document. It will be false if the region is to update + * an existing element in the document. + * + * The 'id' and 'class' attributes of the region may also be modified as well. + * + * @param array $region + * @return string + * + */ + protected function populateRegionDetails(&$region) { + + $modes = array('before', 'after', 'prepend', 'append', 'replace', 'remove'); + $mode = ''; + $isNewElement = false; + $id = isset($region['attrs']['id']) ? $region['attrs']['id'] : ''; + $_id = $id; + $prefix = 'pw-'; + + if(strpos($id, $prefix) === 0) { + // i.e. pw-before-something + foreach($modes as $m) { + if(strpos($id, "$prefix$m-") === 0) { + list($pw, $mode, $id) = explode('-', $id, 3); + if($pw) {} // ignore + break; + } + } + } + + if(!$mode) { + // attempt to get mode from class attribute + $replaceID = ''; + foreach($region['classes'] as $key => $class) { + $replaceID = ''; + if(strpos($class, $prefix) !== 0) continue; + list($pw, $m) = explode('-', $class, 2); + if($pw) {} // ignore + if(strpos($m, '-')) { + // i.e. pw-append-content + $replaceID = $id; + list($m, $id) = explode('-', $m, 2); + $isNewElement = true; + } + if(in_array($m, $modes)) { + $mode = $m; + // remove the class so it doesn't appear in original markup + unset($region['classes'][$key]); + $region['attrs']['class'] = implode(' ', $region['classes']); + break; + } + } + if($replaceID && ($isNewElement || $mode == 'before' || $mode == 'after')) { + $region['attrs']['data-id'] = $replaceID; + } + } + + if(!$mode || !in_array($mode, $modes)) $mode = 'auto'; + + if(!$isNewElement) { + $isNewElement = empty($_id) || $mode == 'before' || $mode == 'after'; + } + + $region['new'] = $isNewElement; + $region['mode'] = $mode; + $region['note'] = "#$id ($mode" . ($isNewElement ? "+new" : "") . ")"; + $region['attrs']['id'] = $id; + + if(count($region['classes'])) $region['note'] .= "." . implode('.', $region['classes']); + + return $mode; + } + + /** + * Is the given HTML markup likely to have regions? + * + * @param string $html + * @return bool + * + */ + public function hasRegions(&$html) { + if(strpos($html, ' id=') === false && strpos($html, 'pw-') === false) return false; + return true; + } + } \ No newline at end of file diff --git a/wire/modules/PageRender.module b/wire/modules/PageRender.module index 84de0691..6231baa4 100644 --- a/wire/modules/PageRender.module +++ b/wire/modules/PageRender.module @@ -533,6 +533,28 @@ class PageRender extends WireData implements Module, ConfigurableModule { $this->renderRecursionLevel--; } + /** + * Populate markup regions directly to $html + * + * @param $html + * + */ + protected function populateMarkupRegions(&$html) { + $pos = stripos($html, 'wire($markupRegions); + $markupRegions->populate($html, $htmlBefore); + } + } + /** * Renders a field value * @@ -671,217 +693,6 @@ class PageRender extends WireData implements Module, ConfigurableModule { return $tpl->render(); } - /** - * Identify and populate markup regions in given HTML - * - * To use this, you must set `$config->useMarkupRegions = true;` in your /site/config.php file. - * In the future it may be enabled by default for any templates with text/html content-type. - * - * This takes anything output before the opening `` that comes after it. For instance, if there's a `
` in the - * document, then a #content element output prior to the doctype will replace it during page render. - * This enables one to use delayed output as if it’s direct output. It also makes every HTML element - * in the output with an “id” attribute a region that can be populated from any template file. It’s - * a good pairing with a `$config->appendTemplateFile` that contains the main markup and region - * definitions, though can be used with or without it. - * - * Beyond replacement of elements, append, prepend, insert before, insert after, and remove are also - * supported via “pw-” prefix classes that you can add. The classes do not appear in the final output - * markup. When performing replacements or modifications to elements, PW will merge the attributes - * so that attributes present in the final output are present, plus any that were added by the markup - * regions. See the examples for more details. - * - * Below are some examples. Note that “main” is used as an example “id” attribute of an element that - * appears in the main document markup, and the examples below focus on manipulating it. The examples - * assume there is a `
` in the _main.php file (appendTemplateFile), and the lines in the - * examples would be output from a template file, which manipulates what would ultimately be output - * when the page is rendered. - * ~~~~~~ - * Replacing and removing elements - *
This replaces the #main div and merges any attributes
- *
This does the same as above
- *
This removes #main completely
- * - * Prepending and appending elements - *

This prepends #main with this p tag

- *

This prepends #main and adds "bar" class to main

- *

This appends #main with this p tag

- *

This appends #main and adds a "foo" class to #main

- *
Appends #main with this text + adds title attribute to #main
- *
Appends #main with this text + removes class “baz” from #main
- * - * Inserting new elements - *

This adds an h2 headline with this text before #main

- *

This adds a footer element with this text after #main

- *
This appends a div.foo to #main with this text
- *
This prepends a div.bar to #main with this text
- * ~~~~~~ - * - * @param $out - * - */ - protected function populateMarkupRegions(&$out) { - - $pos = stripos($out, ' tag - if(!$pos) return; - - $htmlBefore = substr($out, 0, $pos); - if(!strlen(trim($htmlBefore)) || (strpos($htmlBefore, ' id=') === false && strpos($htmlBefore, 'pw-') === false)) return; - $html = substr($out, $pos); - if(strpos($html, ' id=') === false) return; - - $markupFinder = new WireMarkupRegions(); - $regions = $markupFinder->find(".pw-*, id=", $htmlBefore, array( - 'verbose' => true, - 'leftover' => true - )); - - $leftoverMarkup = trim($regions["leftover"]); - unset($regions["leftover"]); - if(!count($regions)) return; - - $xregions = array(); // regions that weren't populated - $populatedIDs = array(); - $numUpdates = 0; - $debug = $this->wire('config')->debug && $this->wire('user')->isSuperuser(); - - foreach($regions as $regionKey => $region) { - - $this->populateMarkupRegionMode($region); - $mode = $region['mode']; - $id = $region['attrs']['id']; - $regionHTML = $region['region']; - $mergeAttr = $region['attrs']; - unset($mergeAttr['id']); - - if($region['new']) { - // element is newly added element not already present - $mergeAttr = array(); - $regionHTML = $region['html']; - $attrs = $region['attrs']; - unset($attrs['id']); - if(isset($attrs['data-id'])) { - $attrs['id'] = $attrs['data-id']; - unset($attrs['data-id']); - } - $attrStr = count($attrs) ? ' ' . $markupFinder->renderAttributes($attrs, false) : ''; - if(!strlen(trim($attrStr))) $attrStr = ''; - $regionHTML = str_replace($region['open'], "<$region[name]$attrStr>", $regionHTML); - } - - // if the id attribute doesn't appear in the html, skip it - if(!$markupFinder->hasAttribute('id', $id, $html)) { - $xregions[$regionKey] = $region; - unset($regions[$regionKey]); - continue; - } - - // update the markup - $html = $markupFinder->update("#$id", $regionHTML, $html, array( - 'mode' => $mode, - 'mergeAttr' => $mergeAttr, - )); - - if($debug) { - $populatedIDs[] = ltrim($regionKey, '#') . " => #$id ($mode)"; - } - - $numUpdates++; - } - - if($debug) { - $bull = "\n • "; - $none = "None"; - $html .= ""; - } - - // if there is any leftover markup, output it above the HTML where it would usually go - if(strlen($leftoverMarkup)) { - $html = $leftoverMarkup . $html; - $numUpdates++; - } - - if($numUpdates) $out = $html; - } - - /** - * Determine the and populate 'mode' for the given region - * - * Note that this may modify the 'id' and/or 'class' attributes of $region - * and it also adds a 'new' property to indicate whether the region is a new - * element that should be added. - * - * @param array $region - * @return string - * - */ - protected function populateMarkupRegionMode(&$region) { - - $modes = array('before', 'after', 'prepend', 'append', 'replace', 'remove'); - $mode = ''; - $isNewElement = false; - $id = isset($region['attrs']['id']) ? $region['attrs']['id'] : ''; - $_id = $id; - $prefix = 'pw-'; - - if(strpos($id, $prefix) === 0) { - // i.e. pw-before-something - foreach($modes as $m) { - if(strpos($id, "$prefix$m-") === 0) { - list($pw, $mode, $id) = explode('-', $id, 3); - if($pw) {} // ignore - break; - } - } - } - - if(!$mode) { - // attempt to get mode from class attribute - $replaceID = ''; - foreach($region['classes'] as $key => $class) { - $replaceID = ''; - if(strpos($class, $prefix) !== 0) continue; - list($pw, $m) = explode('-', $class, 2); - if($pw) {} // ignore - if(strpos($m, '-')) { - // i.e. pw-append-content - $replaceID = $id; - list($m, $id) = explode('-', $m, 2); - $isNewElement = true; - } - if(in_array($m, $modes)) { - $mode = $m; - // remove the class so it doesn't appear in original markup - unset($region['classes'][$key]); - $region['attrs']['class'] = implode(' ', $region['classes']); - break; - } - } - if($replaceID && ($isNewElement || $mode == 'before' || $mode == 'after')) { - $region['attrs']['data-id'] = $replaceID; - } - } - - if(!$mode || !in_array($mode, $modes)) $mode = 'auto'; - - if(!$isNewElement) { - $isNewElement = empty($_id) || $mode == 'before' || $mode == 'after'; - } - - $region['new'] = $isNewElement; - $region['mode'] = $mode; - $region['attrs']['id'] = $id; - - return $mode; - } - /** * Provide a disk cache clearing capability within the module's configuration screen *