mirror of
https://github.com/processwire/processwire.git
synced 2025-08-23 06:44:38 +02:00
Move some code from PageRender.module to WireMarkupRegions.php and make additional updates to WireMarkupRegions, adding support for nested/recursive regions.
This commit is contained in:
@@ -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 `<!DOCTYPE` and connects it to the right places
|
||||
* within the `<html>` that comes after it. For instance, if there's a `<div id='content'>` 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 `<div id=main>` 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
|
||||
* <div id='main'>This replaces the #main div and merges any attributes</div>
|
||||
* <div id='main' class='pw-replace'>This does the same as above</div>
|
||||
* <div id='main' class='pw-remove'>This removes #main completely</div>
|
||||
*
|
||||
* Prepending and appending elements
|
||||
* <div id='main' class='pw-prepend'><p>This prepends #main with this p tag</p></div>
|
||||
* <div id='main' class='pw-prepend bar'><p>This prepends #main and adds "bar" class to main</p></div>
|
||||
* <div id='main' class='pw-append'><p>This appends #main with this p tag</p></div>
|
||||
* <div id='main' class='pw-append foo'><p>This appends #main and adds a "foo" class to #main</p></div>
|
||||
* <div id='main' class='pw-append' title='hello'>Appends #main with this text + adds title attribute to #main</div>
|
||||
* <div id='main' class='pw-append -baz'>Appends #main with this text + removes class “baz” from #main</div>
|
||||
*
|
||||
* Inserting new elements
|
||||
* <h2 class='pw-before-main'>This adds an h2 headline with this text before #main</h2>
|
||||
* <footer class='pw-after-main'><p>This adds a footer element with this text after #main</p></footer>
|
||||
* <div class='pw-append-main foo'>This appends a div.foo to #main with this text</div>
|
||||
* <div class='pw-prepend-main bar'>This prepends a div.bar to #main with this text</div>
|
||||
* ~~~~~~
|
||||
*
|
||||
* @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 = "<!--PW-REGION-DEBUG-->";
|
||||
if(strpos($htmlDocument, $landmark) !== false) {
|
||||
$debugNotes = "<pre>" . $this->wire('sanitizer')->entities($debugNotes) . "</pre>" . $landmark;
|
||||
$htmlDocument = str_replace($landmark, $debugNotes, $htmlDocument);
|
||||
} else {
|
||||
$htmlDocument .= "<!--" . $debugNotes . "\n-->";
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
}
|
@@ -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, '<!DOCTYPE');
|
||||
if($pos === false) {
|
||||
// if no doctype match, attempt an html tag match
|
||||
$pos = stripos($html, '<html');
|
||||
}
|
||||
if($pos) {
|
||||
// split document at doctype/html boundary
|
||||
$htmlBefore = substr($html, 0, $pos);
|
||||
$html = substr($html, $pos);
|
||||
$markupRegions = new WireMarkupRegions();
|
||||
$this->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 `<!DOCTYPE` and connects it to the right places
|
||||
* within the `<html>` that comes after it. For instance, if there's a `<div id='content'>` 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 `<div id=main>` 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
|
||||
* <div id='main'>This replaces the #main div and merges any attributes</div>
|
||||
* <div id='main' class='pw-replace'>This does the same as above</div>
|
||||
* <div id='main' class='pw-remove'>This removes #main completely</div>
|
||||
*
|
||||
* Prepending and appending elements
|
||||
* <div id='main' class='pw-prepend'><p>This prepends #main with this p tag</p></div>
|
||||
* <div id='main' class='pw-prepend bar'><p>This prepends #main and adds "bar" class to main</p></div>
|
||||
* <div id='main' class='pw-append'><p>This appends #main with this p tag</p></div>
|
||||
* <div id='main' class='pw-append foo'><p>This appends #main and adds a "foo" class to #main</p></div>
|
||||
* <div id='main' class='pw-append' title='hello'>Appends #main with this text + adds title attribute to #main</div>
|
||||
* <div id='main' class='pw-append -baz'>Appends #main with this text + removes class “baz” from #main</div>
|
||||
*
|
||||
* Inserting new elements
|
||||
* <h2 class='pw-before-main'>This adds an h2 headline with this text before #main</h2>
|
||||
* <footer class='pw-after-main'><p>This adds a footer element with this text after #main</p></footer>
|
||||
* <div class='pw-append-main foo'>This appends a div.foo to #main with this text</div>
|
||||
* <div class='pw-prepend-main bar'>This prepends a div.bar to #main with this text</div>
|
||||
* ~~~~~~
|
||||
*
|
||||
* @param $out
|
||||
*
|
||||
*/
|
||||
protected function populateMarkupRegions(&$out) {
|
||||
|
||||
$pos = stripos($out, '<!DOCTYPE'); // split before doctype, if present
|
||||
if($pos === false) $pos = stripos($out, '<html'); // if no doctype, split before <html> 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 .= "<!-- " .
|
||||
"\nPW markup regions (debug mode info)" .
|
||||
"\n===================================" .
|
||||
"\nPopulated: " . (count($regions) ? $bull . implode($bull, array_values($populatedIDs)) : $none) .
|
||||
"\nUnused: " . (count($xregions) ? $bull . implode($bull, array_keys($xregions)) : $none) .
|
||||
"\nLeftover: " . strlen($leftoverMarkup) . " bytes" .
|
||||
"\n-->";
|
||||
}
|
||||
|
||||
// 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
|
||||
*
|
||||
|
Reference in New Issue
Block a user