Skip to main content
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Nick Miller
Ben Nadel at InVision In Real Life (IRL) 2019 (Phoenix, AZ) with: Nick Miller

Searching The DOM For Comment Nodes Based On Value And Pseudo-Attributes

By on

A couple of years ago, I wrote a really simple jQuery plugin that searched a given DOM (Document Object Model) node for nested Comment nodes. But, it was an all or nothing approach; meaning, you either got comments or you didn't. It wasn't terribly useful. So, I wanted to see if I could rewrite the plugin in a way that you could query for comments based on the comment text or a pseudo-attribute pair.

See the jQuery.fn.comments() plugin in my GitHub account.

Comment nodes don't have attributes like Element nodes do. However, we can use attribute conventions to create "pseudo attributes" in the comment text:

<!-- id="4" name="Ben Nadel" -->

In this case, the comment has 2 "pseudo attributes":

  • id: "4"
  • name: "Ben Nadel"

If I could then query for these comments based on the name-value pairs, it would make the comments a lot easier to consume from a programmatic standpoint. I already have a reason that I want to do this in AngularJS.

I rewrote my jQuery.fn.comments() plugin, from scratch, to allow for a variety of invocation signatures:

  • comments() - Get all comments in the given node, using a shallow search.
  • comments( true ) - Get all the comments in the given node, using a deep search.
  • comments( value ) - Get all the comments in the given node that exactly match the given value, using a shallow search.
  • comments( value, true ) - Get all the comments in the given node that exactly match the given value, using a deep search.
  • comments( name, value ) - Get all the comments in the given node that contain the given pseudo attribute pair, using a shallow search.
  • comments( name, value, true ) - Get all the comments in the given node that contain the given pseudo attribute pair, using a deep search.

To see how this can be used, take a look at this demo. We're going to start out with comments in the DOM; then, we're going to search for those comments and inject Element nodes:

<!doctype html>
<html>
<head>
	<meta charset="utf-8" />

	<title>
		jQuery.fn.comments()
	</title>

	<style type="text/css">

		div {
			border: 1px solid #CCCCCC ;
			margin: 15px 0px 15px 0px ;
			padding: 1px 20px 1px 20px ;
		}

		p {
			border: 1px solid #FF3399 ;
			margin: 15px 0px 15px 0px ;
			padding: 5px 20px 5px 20px ;
		}

	</style>
</head>
<body>

	<h1>
		jQuery.fn.comments()
	</h1>

	<!-- id = 1 -->

	<div>
		<!-- id = 2 -->
	</div>

	<div>

		<div>
			<!-- id = 3 -->
		</div>

		<!-- id = 4 -->
	</div>

	<script type="text/javascript" src="../vendor/jquery/jquery-2.1.0.min.js"></script>
	<script type="text/javascript" src="../src/jquery.comments.js"></script>
	<script type="text/javascript">

		// Loop over the IDs.
		for ( var i = 1 ; i <= 4 ; i++ ) {

			// Insert actual elements after each found comment (with given attirubte).
			$( "body" ).comments( "id", i, true )
				.after( "<p>ID: " + i + "</p>" )
			;

		}

	</script>

</body>
</html>

As you can see, for each ID in the loop, we find the corresponding comment and then insert a new Paragraph element. When we run the above code, we get the following output:

jQuery.fn.comments() example.

The code for this is in my GitHub account; but, here's the first pass:

;(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 ) {

		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, collectComments( node, isDeepSearch ) );

			}

			node = node.nextSibling;

		}

		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 );

Most of the time, comments aren't all that useful from a JavaScript consumption standpoint. But, I think if the comments could be queried in the same way that regular Elements could be queried, they could be leveraged more easily. Hopefully, I'll be able to follow this up with something that I have in mind.

Want to use code from this post? Check out the license.

Reader Comments

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel