//	jquery.truncator.js
//	HTML Truncator for jQuery

(function($) {

	var trailing_whitespace = true;

	$.fn.truncate = function(options) {

		var opts = $.extend({}, $.fn.truncate.defaults, options);
		
		$(this).each(function() {

			var content_length = $.trim(squeeze($(this).text())).length;
			if (content_length <= opts.max_length)
				return;	// bail early if not overlong

			var actual_max_length = opts.max_length - opts.more.length - 3;	// 3 for " ()"
			var truncated_node = recursivelyTruncate(this, actual_max_length);
			var full_node = $(this).hide();

			truncated_node.insertAfter(full_node);
			
			findNodeForMore(truncated_node).append(' (<a href="#show more content">'+opts.more+'</a>)');
			findNodeForLess(full_node).append(' (<a href="#show less content">'+opts.less+'</a>)');
			
			truncated_node.find('a:last').click(function() {
				truncated_node.hide(); full_node.show(); return false;
			});
			full_node.find('a:last').click(function() {
				truncated_node.show(); full_node.hide(); return false;
			});

		});
	}

	// Note that the " (…more)" bit counts towards the max length – so a max
	// length of 10 would truncate "1234567890" to "12 (…more)".
	$.fn.truncate.defaults = {
		max_length: 100,
		more: 'more',
		less: 'less'
	};

	function recursivelyTruncate(node, max_length) {
		return (node.nodeType == 3) ? truncateText(node, max_length) : truncateNode(node, max_length);
	}

	function truncateNode(node, max_length) {
		var node = $(node);
		var new_node = node.clone().empty();
		var truncatedChild;
		node.contents().each(function() {
			var remaining_length = max_length - new_node.text().length;
			if (remaining_length == 0) return;	// breaks the loop
			truncatedChild = recursivelyTruncate(this, remaining_length);
			if (truncatedChild) new_node.append(truncatedChild);
		});
		return new_node;
	}

	function truncateText(node, max_length) {
		var text = squeeze(node.data);
		if (trailing_whitespace)	// remove initial whitespace if last text
			text = text.replace(/^ /, '');	// node had trailing whitespace.
		trailing_whitespace = !!text.match(/ $/);
		var text = text.slice(0, max_length);
		// Ensure HTML entities are encoded
		text = $('<div/>').text(text).html();
		return text;
	}

	// Collapses a sequence of whitespace into a single space.
	function squeeze(string) {
		return string.replace(/\s+/g, ' ');
	}
	
	// Finds the last, innermost block-level element
	function findNodeForMore(node) {
		var $node = $(node);
		var last_child = $node.children(":last");
		if (!last_child) return node;
		var display = last_child.css('display');
		if (!display || display=='inline') return $node;
		return findNodeForMore(last_child);
	};

	// Finds the last child if it's a p; otherwise the parent
	function findNodeForLess(node) {
		var $node = $(node);
		var last_child = $node.children(":last");
		if (last_child && last_child.is('p')) return last_child;
		return node;
	};

})(jQuery);
