html node to path string. HTML or CSS variants.

by Laurens Maneschijn

HTML

<div class="box">
	<h1>DOM element + parents to string</h1>
	<p>
		Intended mostly for debug output.
	</p>
	<p>
		CSS variant is useful to target an element and use the result to quickly get a working CSS selector for that element.
	</p>
	<p>
		HTML variant is useful to quickly build a simple testhtml for that element with similar parents (on which you can test CSS or JS or other things).
	</p>
	<p>
		If long strings get truncated it might help to reverse the elements in output (see options).
	</p>
</div>

<hr>

<div id="testcontainer" class="box testelements test">
	click some elements to test output:<br>
	<div>
		root div
		<b>b<i>i<u>u</u></i></b>
		<form method="GET">
			someform
			<label class="test1 test2">
				<input 
					id="starttarget" 
					type="checkbox" 
					value="on" 
					checked
					data-someval="test a=&quot;val&quot; ole"
				>
				labeltext
			</label>
		</form>
	</div>
</div>

<hr>

<div id="html" class="box half">
	<div class="box js_options floatright">
		<button class="floatright" data-toggle-next>options</button>
		<div class="hidden">
			<label class="box">
				<input type="checkbox" class="js_options_reverse">
				reverse
			</label>
			<br>

			<label class="box">
				<input type="checkbox" class="js_options_usenodeseparator">
				use custom nodeseparator:
				<textarea class="tasmall js_options_nodeseparator"></textarea>
			</label><br>
		</div>
	</div>
	
	output HTML path:<br>
	<textarea class="js_output"></textarea>
</div>

<div id="css" class="box half">
	<div class="box js_options floatright">
		<button class="floatright" data-toggle-next>options</button>
		<div class="hidden">
			<label class="box">
				<input type="checkbox" class="js_options_reverse">
				reverse
			</label>
			<br>

			<label class="box">
				<input type="checkbox" class="js_options_usenodeseparator">
				use custom nodeseparator:
				<textarea class="tasmall js_options_nodeseparator"></textarea>
			</label><br>
		</div>
	</div>
	
	output CSS...

CSS

* {
	box-sizing: border-box;
}
h1,h2,h3,h4,h5,h6{
	margin: 2px 0 0 0;
	font-size: 1em;
}
p{
	margin: 4px 0;
}

.box, .testelements, .testelements * {
	display: inline-block;
	padding: 2px;
	margin: 2px;
	border: 1px solid #888;
}
.box.half{
	vertical-align: top;
	min-width: 49%;
}
.testelements, .testelements * {
	display: inline-block;
	border-style: dashed;
}
.testelements:hover, .testelements *:hover {
	outline: 1px dotted #f0f;
}

textarea {
	width: 100%;
	height: 100px;
	resize: both;
	font-family: monospace;
	font-size: 9px;
	line-height: 1;
	vertical-align: top;
	white-space: pre;
}
textarea.tasmall{
	width: 40px;
	height: 24px;
	overflow: auto;
	text-decoration: underline;
}
.hidden {display: none;}
.displayblock {display: block;}
.displayinlineblock {display: inline-block;}
.floatright {float: right;}

JavaScript

// ----------------------------------------
// below: element-to-string code
// below that: code for demo UI
// ----------------------------------------

var getNodePathdefaultoptions = {
	nodeseparator: '\n ',
	reverse: false,
}
function getNodePathHTML(node, options) {
	// TODO: html escaping for attribute keys and values?
	var o = {};
	Object.assign(o, getNodePathdefaultoptions);
	if (typeof options === 'object') {
		Object.assign(o, options);
	}
	var s = '';
	s += '<';
	s += node.tagName
	if (node.attributes && node.attributes.length) {
		for (var i=0; i < node.attributes.length; i++) {
			var a = node.attributes[i];
			s += ' ';
			s += a.nodeName;
			if (a.nodeValue !== '') {
				s += '="';
				s += html_attribute_value_escape(a.nodeValue);
				s += '"';
			}
		}
	}
	s += '>';
	if (node.parentNode && node.parentNode.tagName) {
		if (o.reverse) {
			return s + o.nodeseparator + getNodePathHTML(node.parentNode, options);
		} else {
			return getNodePathHTML(node.parentNode, options) + o.nodeseparator + s;
		}
		
	} else {
		return s;
	}
}

function getNodePathCSS(node, options) {
	// TODO: css selector escaping?
	var o = {};
	Object.assign(o, getNodePathdefaultoptions);
	if (typeof options === 'object') {
		Object.assign(o, options);
	}
	var s = '';
	s += node.tagName
	if (node.id) {
		// TODO: css selector escaping?
		s += '#' + node.id;
	}
	if (node.className) {
		// TODO: css selector escaping?
		s += '.' + node.className.replace(/\s+/g, ' ').replace(/^\s+/, '').replace(/\s+$/, '').split(' ').join('.');
//	s += '.' + Array.from(node.classList).join('.')
	}
	if (node.attributes && node.attributes.length) {
		for (var i=0; i < node.attributes.length; i++) {
			var a = node.attributes[i];
			if (a.nodeName === 'id' || a.nodeName === 'class') {
				continue;
			}
			s += ' [';
			s += a.nodeName;
			if (a.nodeValue != '') {
				// TODO: is this the same? [key] or [key=""] ?
				s += '="';
				// css value escape:
				s +=...