My latest attempt tries to handle if the "unlink" file is missing and uses a linkTopicsOnce setting:
(function($) { $.fn.extend({ linktopics: function( settings ) { var self = this; this.settings = $.extend({}, $.linktopics.defaults, settings); this.topics = null; this.trimTopics = function () { 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 ) ); } } }; this.setupRegexes = function () { 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)))" ); } } }; this.linkHtml = function () { 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' ) ); if ( self.settings.linkTopicsOnce ) { self.topics[i].linked = true; break; } } } } } }); }; $.getJSON( self.settings.topicLinkJsonUrl, function(json) { if ( typeof json.topics == "object" ) { self.topics = json.topics; self.stopLinks = new Array(); $.ajax({ type: "GET", url: self.settings.topicUnlinkJsonUrl, dataType: "json", success: function(json){ if ( typeof json.stoplinks == "object" ) { self.stopLinks = json.stoplinks; } self.trimTopics(); self.setupRegexes(); self.linkHtml(); }, error: function(xhr,errType,e) { self.setupRegexes(); self.linkHtml(); } }); } }); } }); $.linktopics = {}; // define global defaults, editable by client $.linktopics.defaults = { topicLinkJsonUrl: "topics_link.json", topicUnlinkJsonUrl: "topics_unlink.json", linkTopicsOnce: true }; })(jQuery);