This hasn't been strenuously tested yet... What little I've put together so far is here: http://cosmicforge.com/topics/
This is my first time using jQuery, and so I'm attempting to make a proper plugin instead of just random functions. The functionality is very focused as written -- it purposefully only links the first occurrence found, but I suppose that part could be made optional -- oh, and it doesn't do any error checking, and it doesn't work if the JSON files are missing, so there's plenty of work still to be done. And for those of you who just want to look at code... jQuery(function($) { $("div.storytext > p").add("div.storytext > ol > li").add("div.storytext > ul > li").linktopics(); }); A single (object) param to be passed to linktopics: { topicLinkJsonUrl: "URL to JSON file with topic data", topicUnlinkJsonUrl: "URL to JSON file with list of topics NOT to link" } JSON for topics to be linked: { "topics": [ { "topic": "jQuery", "link": "http://jquery.com/", "strings": [ "jQuery", "Javascript framework", "John Resig" ] }, ... } JSON for topics NOT to be linked: { "stoplinks": [ "Michael Edmondson" ] } /** * jquery.linktopics.js */ (function($) { $.fn.extend({ linktopics: function( settings ) { this.settings = $.extend({}, $.linktopics.defaults, settings); this.topics = null; var self = this; $.getJSON( self.settings.topicLinkJsonUrl, function(json) { if ( typeof json.topics == "object" ) { self.topics = json.topics; self.stopLinks = new Array(); $.getJSON( self.settings.topicUnlinkJsonUrl, function(json) { if ( typeof json.stoplinks == "object" ) { self.stopLinks = json.stoplinks; } for ( var i = 0; i < self.topics.length; i++ ) { var allowed = true; for ( var j = 0, numStopLinks = self.stopLinks.length; j < numStopLinks; j++ ) { if ( self.stopLinks[j] == self.topics[i].topic ) { allowed = false; break; } } if ( ! allowed ) { void( self.topics.splice( i, 1 ) ); } } for ( var i = 0, numTopics = self.topics.length; i < numTopics; i ++ ) { self.topics[i].linked = false; self.topics[i].strings.sort(function(a,b) { // longer strings come first if ( a.length > b.length ) { return -1; } else if ( a.length < b.length ) { return 1; } else { return 0; } }); self.topics[i].regexes = new Array(self.topics[i].strings.length); for ( var j = 0, numStrings = self.topics[i].strings.length; j < numStrings; j++ ) { self.topics[i].regexes[j] = new RegExp( "(^|\\W)(" + self.topics[i].strings[j].replace( /([\[\]\(\)\.\*\$\^\?\+\\])/g, '\\ $1' ) + ")((?!\\w)(?![^<]*(?:>|<\\/a)))" ); } } self.each(function() { for ( var i = 0, numTopics = self.topics.length; i < numTopics; i++ ) { if ( self.topics[i].linked == false ) { for ( var j = 0, numRegexes = self.topics[i].regexes.length; j < numRegexes; j++ ) { if ( $(this).html().search( self.topics[i].regexes[j] ) != -1 ) { $(this).html( $ (this).html().replace( self.topics[i].regexes[j], '$1<a rel="topic" title="Topic: ' + self.topics[i].topic + '" href="' + self.topics[i].link + '">$2<\/a>$3' ) ); self.topics[i].linked = true; break; } } } } }); }); } }); } }); $.linktopics = {}; // define global defaults, editable by client $.linktopics.defaults = { topicLinkJsonUrl: "topics_link.json", topicUnlinkJsonUrl: "topics_unlink.json" }; })(jQuery);