Skip to content

Instantly share code, notes, and snippets.

@itsekhmistro
Created June 9, 2016 09:24
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save itsekhmistro/9096bc51acb17c3830e447c7d7841fae to your computer and use it in GitHub Desktop.
Save itsekhmistro/9096bc51acb17c3830e447c7d7841fae to your computer and use it in GitHub Desktop.
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