Samuel Williams Friday, 05 February 2010

I recently started experimenting with SyntaxHighlighter for making code on my blog more readable. Generally, SyntaxHighlighter has been great. However one issue I ran into was the number of files that need to be loaded. Each language has its own JavaScript file that must be loaded in order to display that particular language. This leads to slow page loads, even if the majority of the code is cached.

To solve this problem, I've written a dynamic JavaScript and CSS loader using jQuery. It is really very simple.

This class is a helper class to load external code. It assists with this process by allowing code to load asynchronously, and once it has all been loaded, to execute a given function.

function ScriptLoader () {
	this.count = 0;
	this._finishCallback = null;
	this.jobs = []
}

ScriptLoader.prototype._finishLoad = function () {
	this.count -= 1;
	
	if (this.count == 0) {
		if (this.jobs.length > 0) {
			this.loadImmediately(this.jobs.shift());
		} else if (this._finishCallback != null) {
			this._finishCallback();
		}
	}
}

ScriptLoader.prototype.load = function(path) {
	if (this.count == 0)
		this.loadImmediately(path);
	else
		this.jobs.push(path);
}

ScriptLoader.prototype.loadImmediately = function(path) {
	this.count += 1;
	
	var scriptLoader = this;
	
	$.ajax({
			type: "GET",
			url: path,
			success: function (data) {
				scriptLoader._finishLoad();
			},
			dataType: "script",
			cache: true
	});
};

ScriptLoader.prototype.onFinish = function(callback) {
	if (this.count == 0) {
		callback();
	} else {
		this._finishCallback = callback;
	}
};

Here is a simple example of how to use this loader:

var scriptLoader = new ScriptLoader();

scriptLoader.load("teapot.js");
scriptLoader.load("optional1.js");
scriptLoader.load("optional2.js");

scriptLoader.onFinish(function() {
	Teapot.initialize();
});

In this example, the final function Teapot.initialize() will only be run after all the scripts have been loaded. The order of loading scripts is also enforced. This is important, because since the scripts are loaded asynchronously, we don't know when the Teapot will be available.

Here is the code which scans through all <pre> elements and loads the appropriate files:

DefaultSyntaxBrushes = [
	['ActionScript3', 'shBrushAS3', ['as3', 'actionscript3']],
	['Bash/shell', 'shBrushBash', ['bash', 'shell']],
	['C#', 'shBrushCSharp', ['c-sharp', 'csharp']],
	['C++', 'shBrushCpp', ['c++', 'cpp', 'c']],
	['CSS', 'shBrushCss', ['css']],
	['Delphi', 'shBrushDelphi', ['delphi', 'pas', 'pascal']],
	['Diff', 'shBrushDiff', ['diff', 'patch']],
	['Groovy', 'shBrushGroovy', ['groovy']],
	['JavaScript', 'shBrushJScript', ['js', 'jscript', 'javascript']],
	['Java', 'shBrushJava', ['java']],
	['JavaFX', 'shBrushJavaFX', ['jfx', 'javafx']],
	['Perl', 'shBrushPerl', ['perl', 'pl']],
	['PHP', 'shBrushPhp', ['php']],
	['Plain Text', 'shBrushPlain', ['plain', 'text']],
	['PowerShell', 'shBrushPowerShell', ['ps', 'powershell']],
	['Python', 'shBrushPython', ['py', 'python']],
	['Ruby', 'shBrushRuby', ['rails', 'ror', 'ruby']],
	['Scala', 'shBrushScala', ['scala']],
	['SQL', 'shBrushSql', ['sql']],
	['Visual Basic', 'shBrushVb', ['vb', 'vbnet']],
	['XML', 'shBrushXml', ['xml', 'xhtml', 'xslt', 'html']],
	['X86 ASM', 'shBrushX86ASM', ['x86-asm']],
	['AppleScript', 'shBrushAppleScript', ['applescript']]
];

function loadCSS (path) {
	$("head").append("<link>");
	css = $("head").children(":last");
	
	css.attr({
		rel: "stylesheet",
		type: "text/css",
		href: path
	});
}

function setupSyntaxHighlighter (settings) {
	var scriptLoader = new ScriptLoader();
	
	var alias_list = {};
	var core_loaded = false;
	
	$("pre").each(function(pre) {
		var match = this.className.match(/brush: (.+?)(;|$)/);
		if (match)
			alias_list[match[1]] = true;
	});
	
	for (i in DefaultSyntaxBrushes) {
		var brush = DefaultSyntaxBrushes[i];
		var load = false;
		
		for (j in brush[2]) {
			if (alias_list[brush[2][j]])
				load = true;
		}
		
		if (load) {
			if (!core_loaded) {
				scriptLoader.load(settings['root'] + 'shCore.js');
				loadCSS(settings['root'] + 'shCore.css');
				
				for (k in settings['stylesheets']) {
					loadCSS(settings['root'] + settings['stylesheets'][k]);
				}
				core_loaded = true;
			}
			
			scriptLoader.load(settings['root'] + brush[1] + '.js');
		}
	}
	
	if (core_loaded) {
		scriptLoader.onFinish(function() {
			SyntaxHighlighter.highlight();
		});
	}
}

In my main page HTML, I have the following which kicks the whole process off:

<script type="text/javascript" src="/_static/sh/loader.js"></script>
<script type="text/javascript">
	$(function() {
		var SyntaxHighlighterSettings = {
			root: '/_static/sh/',
			stylesheets: ['shThemeDefault.css'],
		};
		
		setupSyntaxHighlighter(SyntaxHighlighterSettings);
	});
</script>

As you can see, due to the script loader, the code is relatively simple. As an alternative, my jQuery.Syntax highlighter dynamically loads all code by default, avoiding the need for additional loader scripts.

Comments

Leave a comment

Please note, comments must be formatted using Markdown. Links can be enclosed in angle brackets, e.g. <www.codeotaku.com>.