This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
core/lib/Drupal/Core/Render/Renderer.php | 19 + | |
modules/renderviz/css/renderviz.layout.css | 19 ++ | |
modules/renderviz/css/renderviz.theme.css | 11 + | |
modules/renderviz/js/jquery.comments.js | 351 +++++++++++++++++++++++++++++ | |
modules/renderviz/js/renderviz.js | 102 +++++++++ | |
modules/renderviz/renderviz.info.yml | 3 + | |
modules/renderviz/renderviz.libraries.yml | 24 ++ | |
modules/renderviz/renderviz.module | 13 ++ | |
8 files changed, 532 insertions(+) | |
diff --git a/core/lib/Drupal/Core/Render/Renderer.php b/core/lib/Drupal/Core/Render/Renderer.php | |
index 09a1bf1..40073c0 100644 | |
--- a/core/lib/Drupal/Core/Render/Renderer.php | |
+++ b/core/lib/Drupal/Core/Render/Renderer.php | |
@@ -11,6 +11,7 @@ | |
use Drupal\Core\Controller\ControllerResolverInterface; | |
use Drupal\Core\Theme\ThemeManagerInterface; | |
use Symfony\Component\HttpFoundation\RequestStack; | |
+use Drupal\Component\Serialization\Json; | |
/** | |
* Turns a render array into a HTML string. | |
@@ -387,6 +388,8 @@ protected function doRender(&$elements, $is_root_call = FALSE) { | |
$elements['#cache']['max-age'] = isset($elements['#cache']['max-age']) ? $elements['#cache']['max-age'] : Cache::PERMANENT; | |
$elements['#attached'] = isset($elements['#attached']) ? $elements['#attached'] : array(); | |
+ $elements['#debug']['#cache'] = $elements['#cache']; | |
+ | |
// Allow #pre_render to abort rendering. | |
if (!empty($elements['#printed'])) { | |
// The #printed element contains all the bubbleable rendering metadata for | |
@@ -551,6 +554,22 @@ protected function doRender(&$elements, $is_root_call = FALSE) { | |
// Rendering is finished, all necessary info collected! | |
$context->bubble(); | |
+ // Add debug output. | |
+ $interesting_keys = ['keys', 'contexts', 'tags', 'max-age']; | |
+ // W3C documentation: HTML Comments are markup, | |
+ // thus it's not allowed to use HTML Comments inside HTML element attributes. | |
+ // Example: <a <!--title="need to be comment out"-->>a link</a> is as wrong as <a <span></span>>a link</a>. | |
+ // Following condition is applied to avoid breaking HTML structure. | |
+ // In result elements that are used as string value of the parent element attribute are skipped. | |
+ // (Example, `form_token` value). | |
+ if ($elements['#markup'] != strip_tags($elements['#markup'])) { | |
+ if (array_intersect(array_keys($elements['#cache']), $interesting_keys) || array_intersect(array_keys($elements['#debug']['#cache']), $interesting_keys)) { | |
+ $prefix = '<!--RENDERER_START-->' . '<!--' . Json::encode($elements['#cache']) . '-->' . '<!--' . Json::encode($elements['#debug']['#cache']) . '-->'; | |
+ $suffix = '<!--RENDERER_END-->'; | |
+ $elements['#markup'] = $prefix . $elements['#markup'] . $suffix; | |
+ } | |
+ } | |
+ | |
$elements['#printed'] = TRUE; | |
return $elements['#markup']; | |
} | |
diff --git a/modules/renderviz/css/renderviz.layout.css b/modules/renderviz/css/renderviz.layout.css | |
new file mode 100644 | |
index 0000000..3231646 | |
--- /dev/null | |
+++ b/modules/renderviz/css/renderviz.layout.css | |
@@ -0,0 +1,19 @@ | |
+html.renderviz { | |
+ perspective: 3000px; | |
+ transition: perspective 1s; | |
+} | |
+ | |
+html.renderviz body { | |
+ transform-style: preserve-3d; | |
+ transform: rotateY(30deg) rotateX(30deg); | |
+} | |
+ | |
+.renderviz-trace-layer { | |
+ transform: translateZ(10px); | |
+ transition: transform 2s, outline 0.5s; | |
+ transform-style: preserve-3d; | |
+} | |
+ | |
+.renderviz-trace-focus { | |
+ transform: rotateX(-30deg) rotateY(-30deg) translateZ(300px); | |
+} | |
diff --git a/modules/renderviz/css/renderviz.theme.css b/modules/renderviz/css/renderviz.theme.css | |
new file mode 100644 | |
index 0000000..7a2886a | |
--- /dev/null | |
+++ b/modules/renderviz/css/renderviz.theme.css | |
@@ -0,0 +1,11 @@ | |
+.renderviz-trace-layer { | |
+ outline: 1px solid red !important; | |
+} | |
+ | |
+.renderviz-trace-layer-root { | |
+ box-shadow: inset 0 0 100px 30px red !important; | |
+} | |
+ | |
+.renderviz-trace-focus { | |
+ box-shadow: inset 0 0 100px 30px blue !important; | |
+} | |
diff --git a/modules/renderviz/js/jquery.comments.js b/modules/renderviz/js/jquery.comments.js | |
new file mode 100644 | |
index 0000000..bca7c37 | |
--- /dev/null | |
+++ b/modules/renderviz/js/jquery.comments.js | |
@@ -0,0 +1,351 @@ | |
+;(function( $ ) { | |
+ | |
+ "use strict"; | |
+ | |
+ // When invoked, the arguments can be defined in several ways: | |
+ // -- | |
+ // .comments() - Gets all child comments. | |
+ // .comments( true ) - Gets all comments (deep search). | |
+ // .comments( value ) - Gets all child comments with the current value. | |
+ // .comments( value, true ) - Gets all comments with the current value (deep search). | |
+ // .comments( name, value ) - Gets all child comments with given name-value pair. | |
+ // .comments( name, value, true ) - Gets all comments with given name-value pair (deep search). | |
+ $.fn.comments = function() { | |
+ | |
+ var settings = normalizeArguments( arguments ); | |
+ | |
+ var comments = []; | |
+ | |
+ // Search for comments in each of the current context nodes. | |
+ for ( var i = 0, length = this.length ; i < length ; i++ ) { | |
+ | |
+ appendAll( | |
+ comments, | |
+ findComments( this[ i ], settings.deep, settings.name, settings.value ) | |
+ ); | |
+ | |
+ } | |
+ | |
+ // If there is more than one comment, make sure the collection is unique. | |
+ if ( comments.length > 1 ) { | |
+ | |
+ comments = $.unique( comments ); | |
+ | |
+ } | |
+ | |
+ // Add the found comments to the stack of jQuery selector execution so that the | |
+ // user can tranverse back up the stack when done. | |
+ return( this.pushStack( comments, "comments", arguments ) ); | |
+ | |
+ }; | |
+ | |
+ | |
+ // --- | |
+ // PRIVATE METHODS. | |
+ // --- | |
+ | |
+ | |
+ // I add all items in the incoming collection to the end of the existing collection. | |
+ // This performs an in-place append; meaning, the existing array is mutated directly. | |
+ function appendAll( existing, incoming ) { | |
+ | |
+ for ( var i = 0, length = incoming.length ; i < length ; i++ ) { | |
+ | |
+ existing.push( incoming[ i ] ); | |
+ | |
+ } | |
+ | |
+ return( existing ); | |
+ | |
+ } | |
+ | |
+ | |
+ // I collect the comment nodes contained within the given root node. | |
+ function collectComments( rootNode, isDeepSearch ) { | |
+ | |
+ // Check for modern browser optimization. | |
+ if ( isDeepSearch && document.createTreeWalker ) { | |
+ | |
+ return( collectCommentsWithTreeWalker( rootNode ) ); | |
+ | |
+ } | |
+ | |
+ return( collectCommentsWithRecursion( rootNode, isDeepSearch ) ); | |
+ | |
+ } | |
+ | |
+ | |
+ // I collect the comment nodes contained within the given root node using the | |
+ // universally supported node API. This method can handle both shallow and deep | |
+ // searches using recursion. | |
+ function collectCommentsWithRecursion( rootNode, isDeepSearch ) { | |
+ | |
+ var comments = []; | |
+ | |
+ var node = rootNode.firstChild; | |
+ | |
+ while ( node ) { | |
+ | |
+ // Is comment node. | |
+ if ( node.nodeType === 8 ) { | |
+ | |
+ comments.push( node ); | |
+ | |
+ // Is element node (and we want to recurse). | |
+ } else if ( isDeepSearch && ( node.nodeType === 1 ) ) { | |
+ | |
+ appendAll( comments, collectCommentsWithRecursion( node, isDeepSearch ) ); | |
+ | |
+ } | |
+ | |
+ node = node.nextSibling; | |
+ | |
+ } | |
+ | |
+ return( comments ); | |
+ | |
+ } | |
+ | |
+ | |
+ // I collect the comment nodes contained within the given root node using the newer | |
+ // TreeWalker API. This method can only handle deep searches since the search cannot | |
+ // be [easily] limited to a single level of the DOM. | |
+ function collectCommentsWithTreeWalker( rootNode ) { | |
+ | |
+ var comments = []; | |
+ | |
+ // NOTE: Last two arguments use default values but are NOT optional in Internet | |
+ // Explorer. As such, we have to use them here for broader support. | |
+ var treeWalker = document.createTreeWalker( rootNode, NodeFilter.SHOW_COMMENT, null, false ); | |
+ | |
+ while ( treeWalker.nextNode() ) { | |
+ | |
+ comments.push( treeWalker.currentNode ); | |
+ | |
+ } | |
+ | |
+ return( comments ); | |
+ | |
+ } | |
+ | |
+ | |
+ // I determine if the given name-value pair is contained within the given text. | |
+ function containsAttribute( text, name, value ) { | |
+ | |
+ if ( ! text ) { | |
+ | |
+ return( false ); | |
+ | |
+ } | |
+ | |
+ // This is an attempt to quickly disqualify the comment value without having to | |
+ // incur the overhead of parsing the comment value into name-value pairs. | |
+ if ( value && ( text.indexOf( value ) === -1 ) ) { | |
+ | |
+ return( false ); | |
+ | |
+ } | |
+ | |
+ // NOTE: Using "==" to allow some type coersion. | |
+ if ( parseAttributes( text )[ name.toLowerCase() ] == value ) { | |
+ | |
+ return( true ); | |
+ | |
+ } | |
+ | |
+ return( false ); | |
+ | |
+ } | |
+ | |
+ | |
+ // I filter the given comments collection based on the existing of a "pseudo" | |
+ // attributes with the given name-value pair. | |
+ function filterCommentsByAttribute( comments, name, value ) { | |
+ | |
+ var filteredComments = []; | |
+ | |
+ for ( var i = 0, length = comments.length ; i < length ; i++ ) { | |
+ | |
+ var comment = comments[ i ]; | |
+ | |
+ if ( containsAttribute( comment.nodeValue, name, value ) ) { | |
+ | |
+ filteredComments.push( comment ); | |
+ | |
+ } | |
+ | |
+ } | |
+ | |
+ return( filteredComments ); | |
+ | |
+ } | |
+ | |
+ | |
+ // I filter the given comments based on the full-text match of the given value. | |
+ // -- | |
+ // NOTE: Leading and trailing white-space is trimmed from the node content before | |
+ // being compared to the given value. | |
+ function filterCommentsByText( comments, value ) { | |
+ | |
+ var filteredComments = []; | |
+ | |
+ var whitespace = /^\s+|\s+$/g; | |
+ | |
+ for ( var i = 0, length = comments.length ; i < length ; i++ ) { | |
+ | |
+ var comment = comments[ i ]; | |
+ var text = ( comment.nodeValue || "" ).replace( whitespace, "" ); | |
+ | |
+ if ( text === value ) { | |
+ | |
+ filteredComments.push( comment ); | |
+ | |
+ } | |
+ | |
+ } | |
+ | |
+ return( filteredComments ); | |
+ | |
+ } | |
+ | |
+ | |
+ | |
+ // I find the comments in the given node using the given, normalized settings. | |
+ function findComments( node, isDeepSearch, name, value ) { | |
+ | |
+ var comments = collectComments( node, isDeepSearch ); | |
+ | |
+ if ( name ) { | |
+ | |
+ return( filterCommentsByAttribute( comments, name, value ) ); | |
+ | |
+ } else if ( value ) { | |
+ | |
+ return( filterCommentsByText( comments, value ) ); | |
+ | |
+ } | |
+ | |
+ return( comments ); | |
+ | |
+ } | |
+ | |
+ | |
+ // I convert the invocation arguments into a normalized settings hash that the search | |
+ // algorithm can use with confidence. | |
+ function normalizeArguments( argumentCollection ) { | |
+ | |
+ if ( argumentCollection.length > 3 ) { | |
+ | |
+ throw( new Error( "Unexpected number of arguments." ) ); | |
+ | |
+ } | |
+ | |
+ if ( ! argumentCollection.length ) { | |
+ | |
+ return({ | |
+ deep: false, | |
+ name: "", | |
+ value: "" | |
+ }); | |
+ | |
+ } | |
+ | |
+ if ( argumentCollection.length === 3 ) { | |
+ | |
+ return({ | |
+ deep: !! argumentCollection[ 2 ], | |
+ name: argumentCollection[ 0 ], | |
+ value: argumentCollection[ 1 ] | |
+ }); | |
+ | |
+ } | |
+ | |
+ var lastValue = Array.prototype.pop.call( argumentCollection ); | |
+ | |
+ if ( ( lastValue === true ) || ( lastValue === false ) ) { | |
+ | |
+ if ( ! argumentCollection.length ) { | |
+ | |
+ return({ | |
+ deep: lastValue, | |
+ name: "", | |
+ value: "" | |
+ }); | |
+ | |
+ } | |
+ | |
+ if ( argumentCollection.length === 1 ) { | |
+ | |
+ return({ | |
+ deep: lastValue, | |
+ name: "", | |
+ value: argumentCollection[ 0 ] | |
+ }); | |
+ | |
+ } | |
+ | |
+ if ( argumentCollection.length === 2 ) { | |
+ | |
+ return({ | |
+ deep: lastValue, | |
+ name: argumentCollection[ 0 ], | |
+ value: argumentCollection[ 1 ] | |
+ }); | |
+ | |
+ } | |
+ | |
+ } | |
+ | |
+ if ( ! argumentCollection.length ) { | |
+ | |
+ return({ | |
+ deep: false, | |
+ name: "", | |
+ value: lastValue | |
+ }); | |
+ | |
+ } | |
+ | |
+ if ( argumentCollection.length === 1 ) { | |
+ | |
+ return({ | |
+ deep: false, | |
+ name: argumentCollection[ 0 ], | |
+ value: lastValue | |
+ }); | |
+ | |
+ } | |
+ | |
+ if ( argumentCollection.length === 2 ) { | |
+ | |
+ return({ | |
+ deep: false, | |
+ name: argumentCollection[ 1 ], | |
+ value: lastValue | |
+ }); | |
+ | |
+ } | |
+ | |
+ } | |
+ | |
+ | |
+ // I parse the given text value into a collection of name-value pairs. | |
+ function parseAttributes( text ) { | |
+ | |
+ var attributes = {}; | |
+ | |
+ var pairPattern = /([a-zA-Z][^=\s]*)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s]+)))?/gi; | |
+ | |
+ var matches = null; | |
+ | |
+ while ( matches = pairPattern.exec( text ) ) { | |
+ | |
+ attributes[ matches[ 1 ].toLowerCase() ] = ( matches[ 2 ] || matches[ 3 ] || matches[ 4 ] || "" ); | |
+ | |
+ } | |
+ | |
+ return( attributes ); | |
+ | |
+ } | |
+ | |
+})( jQuery ); | |
diff --git a/modules/renderviz/js/renderviz.js b/modules/renderviz/js/renderviz.js | |
new file mode 100644 | |
index 0000000..1a127e4 | |
--- /dev/null | |
+++ b/modules/renderviz/js/renderviz.js | |
@@ -0,0 +1,102 @@ | |
+(function ($, _, JSON) { | |
+ | |
+ "use strict"; | |
+ | |
+ // Map the metadata to data- attributes. | |
+ function setMetadata(commentNode, element) { | |
+ var metadata = JSON.parse(commentNode.textContent); | |
+ for (var key in metadata) { | |
+ var value = metadata[key]; | |
+ if (value instanceof Array) { | |
+ if (value.length === 0) { | |
+ continue; | |
+ } | |
+ value = value.join(' '); | |
+ } | |
+ element.setAttribute('data-renderviz-' + key, value); | |
+ } | |
+ } | |
+ | |
+ // Map the pre-bubbling metadata to data- attributes. | |
+ function setPrebubblingMetadata(commentNode, element) { | |
+ var prebubblingMetadata = JSON.parse(commentNode.textContent); | |
+ for (var key in prebubblingMetadata) { | |
+ var value = prebubblingMetadata[key]; | |
+ if (value instanceof Array) { | |
+ if (value.length === 0) { | |
+ continue; | |
+ } | |
+ value = value.join(' '); | |
+ } | |
+ element.setAttribute('data-renderviz-prebubbling-' + key, value); | |
+ } | |
+ } | |
+ | |
+ function visualize(queryMetadataType, queryMetadataValue) { | |
+ // One-time initialization. | |
+ $('html').once('renderviz').addClass('renderviz'); | |
+ // Reset. | |
+ $('.renderviz-trace-layer').removeClass('renderviz-trace-layer'); | |
+ $('.renderviz-trace-layer-root').removeClass('renderviz-trace-layer-root'); | |
+ $('.renderviz-trace-focus').removeClass('renderviz-trace-focus'); | |
+ // Apply the new query. | |
+ var result = $('[data-renderviz-' + queryMetadataType + '~="' + queryMetadataValue + '"]'); | |
+ console.log('' + result.length + ' matches found.'); | |
+ console.log(result); | |
+ result.addClass('renderviz-trace-layer'); | |
+ $('[data-renderviz-prebubbling-' + queryMetadataType + '~="' + queryMetadataValue + '"]').addClass('renderviz-trace-layer-root'); | |
+ window.rendervizLastType = queryMetadataType; | |
+ window.rendervizLastValue = queryMetadataValue; | |
+ } | |
+ | |
+ function focus(index) { | |
+ // Reset. | |
+ $('.renderviz-trace-focus').removeClass('renderviz-trace-focus'); | |
+ // Apply the new focus. | |
+ var el = $('[data-renderviz-' + window.rendervizLastType + '~="' + window.rendervizLastValue+ '"]')[index]; | |
+ console.log(el); | |
+ $(el).addClass('renderviz-trace-focus'); | |
+ } | |
+ | |
+ Drupal.behaviors.renderviz = { | |
+ attach: function (context) { | |
+ // Transplant the data from the HTML comments onto the parent element. | |
+ var comments = $(context).comments(true); | |
+ for (var i = 0; i < comments.length; i++) { | |
+ if (comments[i].textContent === 'RENDERER_START') { | |
+ var element = comments[i].nextElementSibling; | |
+ if (element) { | |
+ // Mark the element for renderviz treatment. | |
+ element.setAttribute('data-renderviz-element', true); | |
+ setMetadata(comments[i+1], element); | |
+ setPrebubblingMetadata(comments[i+2], element); | |
+ } | |
+ // @todo improve this; might need some complex merging logic. | |
+ // If we have renderer metadata, but it's not for an Element node, | |
+ // then it is for a Text node. In that case, set the pre-bubbling | |
+ // metadata of the Text node on the parent Element node. | |
+ // e.g. TimestampFormatter — the node timestamp | |
+ else { | |
+ element = comments[i].parentElement; | |
+ setPrebubblingMetadata(comments[i+2], element); | |
+ } | |
+ } | |
+ } | |
+ | |
+ $('body').once('renderviz-init').each(function() { | |
+ var contexts = [], tags = []; | |
+ $('[data-renderviz-contexts]').each(function (index, element) { | |
+ contexts = _.union(contexts, element.attributes['data-renderviz-contexts'].value.split(' ')); | |
+ }); | |
+ $('[data-renderviz-tags]').each(function (index, element) { | |
+ tags = _.union(tags, element.attributes['data-renderviz-tags'].value.split(' ')); | |
+ }); | |
+ console.log('' + $('[data-renderviz-element]').length + ' unique rendered elements on the page.', "\nContexts:", contexts, "\nTags:", tags); | |
+ console.log("To use:\n- Querying: `renderviz(metadataType, metadataValue)`, e.g. `renderviz('contexts', 'timezone')`.\n- Focusing: `rendervizFocus(index)`, e.g. `rendervizFocus(0)` to focus on the first element of the last query."); | |
+ window.renderviz = visualize; | |
+ window.rendervizFocus = focus; | |
+ }); | |
+ } | |
+ }; | |
+ | |
+})(jQuery, _, window.JSON); | |
diff --git a/modules/renderviz/renderviz.info.yml b/modules/renderviz/renderviz.info.yml | |
new file mode 100644 | |
index 0000000..08030b1 | |
--- /dev/null | |
+++ b/modules/renderviz/renderviz.info.yml | |
@@ -0,0 +1,3 @@ | |
+name: Render Visualization | |
+type: module | |
+core: 8.x | |
diff --git a/modules/renderviz/renderviz.libraries.yml b/modules/renderviz/renderviz.libraries.yml | |
new file mode 100644 | |
index 0000000..ba4aafd | |
--- /dev/null | |
+++ b/modules/renderviz/renderviz.libraries.yml | |
@@ -0,0 +1,22 @@ | |
+jquery.comments: | |
+ remote: … | |
+ version: … | |
+ license: | |
+ name: … | |
+ url: … | |
+ gpl-compatible: true | |
+ js: | |
+ js/jquery.comments.js: {} | |
+ dependencies: | |
+ - core/jquery | |
+ - core/underscore | |
+ | |
+renderviz: | |
+ css: | |
+ layout: | |
+ css/renderviz.layout.css: {} | |
+ theme: | |
+ css/renderviz.theme.css: {} | |
+ js: | |
+ js/renderviz.js: {} | |
+ dependencies: | |
+ - core/underscore | |
+ - renderviz/jquery.comments | |
diff --git a/modules/renderviz/renderviz.module b/modules/renderviz/renderviz.module | |
new file mode 100644 | |
index 0000000..0945f5d | |
--- /dev/null | |
+++ b/modules/renderviz/renderviz.module | |
@@ -0,0 +1,13 @@ | |
+<?php | |
+ | |
+/** | |
+ * @file | |
+ * … | |
+ */ | |
+ | |
+/** | |
+ * Implements hook_page_attachments(). | |
+ */ | |
+function renderviz_page_attachments(array &$page) { | |
+ $page['#attached']['library'][] = 'renderviz/renderviz'; | |
+} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment