(function($, window, hljs){ // one can become many var painters = 1, waitForLoad = false, waitForSource = false; // tool to parse/manipulate hash var hashTool = { get: function(id) { var hash = window.location.hash; if (hash.length > 0) { var match = hash.match(new RegExp(id+":{([a-zA-Z0-9,-]*)}")); if (match) { return match[1].split(","); } } return []; }, set: function(id, hl) { var hash = window.location.hash, newHash, addHash = id+":{"+hl.join(",")+"}", match = hash.indexOf(id+":{"); if (hl.length === 0) { return this.remove(id); } if (match !== -1) { newHash = hash.replace( new RegExp("("+id+":{[a-zA-Z0-9,-]*})"), addHash ); } else { newHash = (hash.length > 0) ? hash+","+addHash : addHash; } window.location.hash = newHash; }, remove: function(id) { window.location.hash = window.location.hash.replace( new RegExp("([,]?"+id+":{[a-zA-Z0-9,-]*}[,]?)"), "" ); } }; // precompile regex patterns var rxp = { numberRange: /^([0-9]+)-([0-9]+)$/, pageNumber: /-([0-9]+)$/, multilineBegin: /(?:.[^<]*(?!<\/span>)|)$/ig, multilineEnd: /()?(?:.[^<]*)?(<\/span>)/ig }; // hey vincent! $.fn.vanGogh = function(options){ var defaults = { language: "auto", firstLine: 1, maxLines: 0, numbers: true, highlight: null, animateGutter: true, autoload: "http://softwaremaniacs.org/media/soft/highlight/highlight.pack.js", tab: " " }; // merge defaults and passed options options = $.extend({}, defaults, options); // scope vars var elems = this, run = 0, remoteData; // cross-browser compatible selection function selectCode(elm) { var w = window, d = window.document; if (d.body.createTextRange) { var range = d.body.createTextRange(); range.moveToElementText(elm); range.select(); } else if (d.createRange) { var sel = w.getSelection(), range = d.createRange(); range.selectNodeContents(elm); sel.removeAllRanges(); sel.addRange(range); } } // this function puts Van Gogh into action function paint() { // check if we're waiting for ajax request if (waitForLoad || waitForSource) { setTimeout(paint, 100); return; } run++; // abort if run 10 times or more if (run >= 10) { return; } // load remote source if (options.source && !remoteData) { waitForSource = true; $.ajax({ url: options.source, crossDomain: true, dataType: "text", success: function(result){ remoteData = result; }, error: function(xhr, textStatus){ remoteData = "ERROR: "+textStatus; }, complete: function() { waitForSource = false; paint(); } }); return; } hljs = hljs || window.hljs; if (!hljs) { // autoload highlight.js waitForLoad = true; $.getScript(options.autoload, function(){ waitForLoad = false; paint(); }); return; } // iterate passed elements elems.filter("pre,code").each(function(){ var self = $(this) .addClass("vg-container") .attr("id", this.id || "vg-"+painters++), id = this.id, container = self.find("code"), inline = false, lastClicked = false, highlighted = []; // if there's no code element, // assume it's self and inline if (!container.length) { container = self; inline = true; } // put remote data in container (options.source && remoteData) && container.text(remoteData); // copy the original text var original = container.text(); // fire off highlight.js hljs.highlightBlock(container[0], options.tab); // split the result into lines so that we can process them var lines = container.html().split("\n"), numbers = "", code = ""; // highlight a line/word/phrase function highlight(num, clear, initial){ var range = false, lines = self.find(".vg-line"); // clear all previous highlights if (clear) { // remove class self.find(".vg-highlight").removeClass("vg-highlight"); // remove from hash hashTool.remove(id); // clear highlighted array highlighted = []; } // if not array, make it into one num = ($.type(num) === "array") ? num : [num]; // iterate array $.each(num, function(i, hl){ // if already highlighted, do nothing if (highlighted.indexOf(hl) > -1) { return; } // convert to int if string is number if (!isNaN(parseFloat(hl, 10)) && isFinite(hl)) { hl = parseInt(hl, 10); } // handle strings if ($.type(hl) === "string") { // check for range var match = rxp.numberRange.exec(hl); if (match) { var from = match[1], to = match[2], range = ""; for (var i = from; i <= to; i++) { range += ',#'+id+'-'+i; highlighted.push(i); } lines.filter(range.substring(1)).addClass("vg-highlight"); } else { // check for word/phrase self.find(".vg-line:contains("+hl+")").each(function(){ var line = $(this).addClass("vg-highlight"); line.html(line.html().replace(hl, ''+hl+'')); }); highlighted.push(hl); } } else { var lineId = id+'-'+this, line = lines.filter('#'+lineId); // line found if (line.length) { line.addClass("vg-highlight"); highlighted.push(hl); } } }); // update hash !initial && hashTool.set(id, highlighted); } // if not inline if (!inline) { // iterate the lines var multiline = {}, level = 0; $.each(lines, function(i, line){ var num = i+options.firstLine, lineId = id+'-'+num, newLine = line; // if numbers is enabled, add number to gutter if (options.numbers) { numbers += ''+num+''; } // check if in multiline mode if (multiline[level]) { // check for closing tag var end = rxp.multilineEnd.exec(line); // simulate a negative lookbehind by forcing first group not to match if (end && !end[1]) { // closing tag found newLine = ''+newLine; // down a level delete multiline[level]; level--; } else { // we're still on the same level newLine = ''+newLine+''; } } // detect and retain multiline styles // (inline languages, multi-line comments etc.) var match = rxp.multilineBegin.exec(line); if (match) { // up a level level++; // store current style multiline[level] = match[1]; } // wrap the line code += '
'+newLine+'
'; }); // wrap all lines code = ''+code+''; // add gutter to container if numbers is enabled if (options.numbers) { code = '
'+numbers+'
'+code; } // put new code in container self.html(code); // reset the container since we just replaced the original element container = self.find("code"); // we want numbersto be clickable self.find(".vg-number").click(function(e){ var number = $(this), rel = number.attr("rel"), line = self.find(rel); // check if already highlighted if (line.hasClass("vg-highlight")) { // remove highlight class line.removeClass("vg-highlight"); // remove from highlighted highlighted.splice(highlighted.indexOf(number.text()), 1); // update hash hashTool.set(id, highlighted); lastClicked = false; return false; } var prevClicked = lastClicked; lastClicked = parseInt(rxp.pageNumber.exec(rel)[1], 10); // handle shift-click to allow selecting range if (e.shiftKey && lastClicked) { highlight( prevClicked < lastClicked ? prevClicked+'-'+lastClicked : lastClicked+'-'+prevClicked, true ); } else { // handle ctrl-click to allow multiple highlightings highlight(lastClicked, e.ctrlKey ? false : true); } return false; }); var gutter = self.find(".vg-gutter"), gutterWidth = gutter.outerWidth(), oldLeft = 0, scrollTimer = false; // animate gutter on horizontal scroll if (options.animateGutter) { self.scroll(function(e){ if (this.scrollLeft === oldLeft) { return; } else if (this.scrollLeft <= gutterWidth) { oldLeft = this.scrollLeft; clearTimeout(scrollTimer); scrollTimer = false; gutter.css({ "float": "", "position": "", "left": "" }).show(); } else if (this.scrollLeft < oldLeft) { oldLeft = this.scrollLeft; gutter.hide(); } else if (this.scrollLeft !== oldLeft) { if (scrollTimer) { return; } var elm = this; oldLeft = this.scrollLeft; scrollTimer = setTimeout(function(){ scrollTimer = false; var scrollLeft = elm.scrollLeft; container.css("marginLeft", gutterWidth); gutter.css({ "float": "none", "position": "absolute", "left": scrollLeft-gutterWidth }).show().stop().animate({ left: scrollLeft }); }, 500); } }); } } else if (inline) { self.addClass("vg-code"); } // double-clicking the container will select all code (if supported) container.dblclick(function(){ selectCode(container[0]); return false; }); if (options.maxLines > 0) { var lineHeight = self.find(".vg-line").height(), padding = parseInt(container.css("paddingTop")), newHeight = lineHeight*(options.maxLines+1)+padding; self.css({ minHeight: lineHeight+padding, maxHeight: newHeight }); } // highlight rows passed in options options.highlight && highlight(options.highlight, true, true); // highlight lines that exist in hash var hashLines = hashTool.get(id); hashLines.length && highlight(hashLines, false, true); }); } // let the master begin paint(); // return elements return elems; } })(jQuery, this, (typeof this.hljs !== "undefined") ? this.hljs : false);