Prism - Code Folding plugin prototype

I am using Prism on my blog for my syntax highlighting and I am very happy and thankful about it. Sometimes I wanted to show the users of my blog a specific function of a class, but without decoupling the function from the class. So I needed something like a Prism plugin that can hide specific lines of the code in a foldable area.

Demo

A small demo of my first prototype of a Prism code folding plugin that hides all functions except the function getFoldingeFunction. Here the current source of prism-code-folding.js with data-folding-open="1-103,7-16" and data-folding-closed="3-5,18-55,57-83,85-101" on the pre element.

(function(){

if (typeof self === 'undefined' || !self.Prism || !self.document || !document.querySelector) {
	return;
}

function getFoldingeFunction(container) {
	return function() {
		if (container.className === "prism-folding prism-folding-closed") {
			container.className = "prism-folding";
			return false;
		}
		container.className = "prism-folding prism-folding-closed";
		return false;
	};
};

function createFoldings(newLinesNodes, lines, closed) {
    var ranges = lines.replace(/\s+/g, '').split(',');
	for (var i = 0; i < ranges.length; i++) {
		var range = ranges[i].split('-');
		var start = range[0];
		var end = range[1] - 1;
		var divFoldingContainer = document.createElement('span');
		divFoldingContainer.className = "prism-folding";
		if (closed) {
			divFoldingContainer.className = "prism-folding prism-folding-closed";
		}
		var divFoldingNode = document.createElement('span');
		divFoldingNode.className = "prism-folding-node";
		divFoldingContainer.appendChild(divFoldingNode);
		var parent = newLinesNodes[start].nextSibling.parentNode;
		parent.insertBefore(divFoldingContainer, newLinesNodes[start].nextSibling);
		var currentNode = divFoldingContainer.nextSibling;
		while (currentNode !== newLinesNodes[end]) {
			movingNode = currentNode;
			currentNode = currentNode.nextSibling;
			divFoldingNode.appendChild(movingNode);
		}
		divFoldingNode.appendChild(newLinesNodes[end]);

		var foldingFunction = getFoldingeFunction(divFoldingContainer);

		var arrowDown = document.createElement("span");
		arrowDown.appendChild(document.createTextNode("\u25BD"));
		arrowDown.className = "prism-folding-arrow prism-folding-arrow-down";
		divFoldingContainer.appendChild(arrowDown);
		arrowDown.addEventListener('click', foldingFunction);

		var arrowUp = document.createElement("span");
		arrowUp.appendChild(document.createTextNode("\u25B3"));
		arrowUp.className = "prism-folding-arrow prism-folding-arrow-up";
		divFoldingContainer.appendChild(arrowUp);
		arrowUp.addEventListener('click', foldingFunction);
	}
}

function splitAndReturnNewLineElements(element) {
	var children = element.childNodes;
	var newLinesNodes = [];
	var newLineCounter = 0;
	for (var i = 0; i < children.length; i++) {
		var child = children[i];
		if (child.nodeType === Node.TEXT_NODE) {
			var textNodeValue = child.nodeValue;
			var splitOnNewLine = textNodeValue.split("\n");
			if (splitOnNewLine.length > 1) {
				var siblingBefore = child;
				var parent = child.parentNode;
				child.nodeValue = splitOnNewLine[0];
				for (var j = 1; j < splitOnNewLine.length; j++) {
					var newLineNode = document.createTextNode("\n");
					parent.insertBefore(newLineNode, siblingBefore.nextSibling);
					newLinesNodes[++newLineCounter] = newLineNode;
					var splitResultNode = document.createTextNode(splitOnNewLine[j]);
					parent.insertBefore(splitResultNode, newLineNode.nextSibling);
					i += 2;
					siblingBefore = splitResultNode;
				}
			}
		}
	}
	return newLinesNodes;
}

Prism.hooks.add('complete', function(env) {
    var pre = env.element.parentNode;
    var foldingOpenLines = pre && pre.getAttribute('data-folding-open');
    var foldingClosedLines = pre && pre.getAttribute('data-folding-closed');

    if (!pre || !(foldingOpenLines || foldingClosedLines) || !/pre/i.test(pre.nodeName)) {
        return;
    }
	var element = env.element;
	var newLinesNodes = splitAndReturnNewLineElements(element);
	if (foldingOpenLines) {
    	createFoldings(newLinesNodes, foldingOpenLines);
	}
	if (foldingClosedLines) {
	    createFoldings(newLinesNodes, foldingClosedLines, true);
	}
});

})();

An other small example is the source of prism-code-folding.css with data-folding-open="1-3,4-10,11-14,15-17,18-20,21-23,24-26" on the pre tag.

.prism-folding {
    position: relative;
}
.prism-folding > .prism-folding-arrow {
    position: absolute;
    left: -0.5em;
    text-decoration: none;
    opacity: 0.5;
    line-height: 1em;
}
.prism-folding > .prism-folding-arrow:hover {
    cursor: pointer;
    opacity: 1;
}
.prism-folding > .prism-folding-arrow-down {
    top: -1em;
}
.prism-folding-closed > .prism-folding-node {
    display: none;
}
.prism-folding-closed > .prism-folding-arrow {
    transform: rotate(270deg);
}

Main idea

Prism is highlighting code inside of a code tag with span tokens that have specific classes. The character for a new line \n is often inside of a text node. The main idea is to split the next nodes that contain a new line character into three parts. The part before the new line character, the new line character itself and the part behind the new line character. Afterwards I take all nodes between a given starting line and a given ending line and wrap them in a div element. This div can change the display style from none to block with the help of two arrows at the top and the bottom of the div.

How to use

Obviously, this is supposed to work only for code blocks (<pre><code>) and not for inline code.

You specify the lines to be folded through the data-folding-open attribute for an open folding or the data-folding-closed attribute for an closed folding on the <pre> element, in the following simple format:

  • Ranges are denoted by two numbers, separated with a hyphen (-)
  • Multiple line numbers or ranges are separated by commas.
  • Whitespace is allowed anywhere and will be stripped off.

Examples:

1-5
Lines 1 through 5
1-2, 9-20
Lines 1 through 2, lines 9 through 20

Known Issues

As mentioned above this is just a prototype to see if it can get an offical Prism plugin in the future. There are some known issues at the moment.

  • the coding style is ugly and unreadable
  • the plugin is not tested with other Prism plugins
  • there is not a single test at the moment
Next Previous