2 * SearchHighlight plugin for jQuery
4 * Thanks to Scott Yang <http://scott.yang.id.au/>
5 * for the original idea and some code
7 * @author Renato Formato <rformato@gmail.com>
9 * @version 0.37 (9/1/2009)
12 * - exact (string, default:"exact")
13 * "exact" : find and highlight the exact words.
14 * "whole" : find partial matches but highlight whole words
15 * "partial": find and highlight partial matches
17 * - tag_name (string, default:'span')
18 * The tag that is used to wrap the matched words
20 * - style_name (string, default:'hilite')
21 * The class given to the tag wrapping the matched words.
23 * - style_name_suffix (boolean, default:true)
24 * If true a different number is added to style_name for every different matched word.
26 * - debug_referrer (string, default:null)
27 * Set a referrer for debugging purpose.
29 * - engines (array of regex, default:null)
30 * Add a new search engine regex to highlight searches coming from new search engines.
31 * The first element is the regex to match the domain.
32 * The second element is the regex to match the query string.
33 * Ex: [/^http:\/\/my\.site\.net/i,/search=([^&]+)/i]
35 * - highlight (string, default:null)
36 * A jQuery selector or object to set the elements enabled for highlight.
37 * If null or no elements are found, all the document is enabled for highlight.
39 * - nohighlight (string, default:null)
40 * A jQuery selector or object to set the elements not enabled for highlight.
41 * This option has priority on highlight.
43 * - keys (string, default:null)
44 * Disable the analisys of the referrer and search for the words given as argument
46 * - min_length (number, default:null)
47 * Set the minimun length of a key
53 jQuery
.fn
.SearchHighlight = function(options
) {
54 var ref
= options
.debug_referrer
|| document
.referrer
;
55 if(!ref
&& options
.keys
==undefined) return this;
57 SearchHighlight
.options
= $.extend({exact
:"exact",tag_name
:'span',style_name
:'hilite',style_name_suffix
:true},options
);
59 if(options
.engines
) SearchHighlight
.engines
.unshift(options
.engines
);
60 var q
= SearchHighlight
.splitKeywords(options
.keys
!=undefined?options
.keys
.toLowerCase():SearchHighlight
.decodeURL(ref
,SearchHighlight
.engines
));
62 SearchHighlight
.buildReplaceTools(q
);
63 if(!SearchHighlight
.regex
) return this;
64 return this.each(function(){
66 if(el
==document
) el
= $("body")[0];
67 SearchHighlight
.hiliteElement(el
);
72 var SearchHighlight
= {
76 [/^http:\/\/(www\.)?google\./i, /q=([^&]+)/i], // Google
77 [/^http:\/\/(www\.)?search\.yahoo\./i, /p=([^&]+)/i], // Yahoo
78 [/^http:\/\/(www\.)?search\.msn\./i, /q=([^&]+)/i], // MSN
79 [/^http:\/\/(www\.)?search\.live\./i, /query=([^&]+)/i], // MSN Live
80 [/^http:\/\/(www\.)?search\.aol\./i, /userQuery=([^&]+)/i], // AOL
81 [/^http:\/\/(www\.)?ask\.com/i, /q=([^&]+)/i], // Ask.com
82 [/^http:\/\/(www\.)?altavista\./i, /q=([^&]+)/i], // AltaVista
83 [/^http:\/\/(www\.)?feedster\./i, /q=([^&]+)/i], // Feedster
84 [/^http:\/\/(www\.)?search\.lycos\./i, /q=([^&]+)/i], // Lycos
85 [/^http:\/\/(www\.)?alltheweb\./i, /q=([^&]+)/i], // AllTheWeb
86 [/^http:\/\/(www\.)?technorati\.com/i, /([^\?\/]+)(?:\?.*)$/i] // Technorati
89 decodeURL: function(URL
,reg
) {
90 //try to properly escape not UTF-8 URI encoded chars
92 URL
= decodeURIComponent(URL
);
97 $.each(reg
,function(i
,n
){
99 var match
= URL
.match(n
[1]);
101 query
= match
[1].toLowerCase();
109 splitKeywords: function(query
) {
111 //do not split keywords enclosed by "
112 var m
= query
.match(/"([^"]*)"/g);
114 for(var i
=0, ml
=m
.length
;i
<ml
;i
++) {
115 var i
= query
.indexOf(m
[i
]);
116 query
= query
.substring(0,i
)+'@@@'+i
+'@@@'+query
.substring(i
+m
[i
].length
)
117 m
[i
] = decodeURI(m
[i
]);
118 m
[i
] = m
[i
].split("+").join(' ');
120 query
= query
.split(/[\s,\+]+/);
122 for(var i
=0,l
= query
.length
;i
<l
;i
++) {
123 for(var j
=0, ml
=m
.length
;j
<ml
;j
++) {
124 var regex
= new RegExp("@@@"+j
+"@@@");
125 query
[i
] = query
[i
].replace(regex
,m
[j
].substring(1,m
[j
].length
-1))
133 [/[\xC0-\xC5\u0100-\u0105]/ig,'a'],
134 [/[\xC7\u0106-\u010D]/ig,'c'],
135 [/[\xC8-\xCB]/ig,'e'],
136 [/[\xCC-\xCF]/ig,'i'],
139 [/[\xD2-\xD6\xD8]/ig,'o'],
140 [/[\u015A-\u0161]/ig,'s'],
141 [/[\u0162-\u0167]/ig,'t'],
142 [/[\xD9-\xDC]/ig,'u'],
144 [/[\x91\x92\u2018\u2019]/ig,'\'']
146 matchAccent
: /[\x91\x92\xC0-\xC5\xC7-\xCF\xD1-\xD6\xD8-\xDC\xFF\u0100-\u010D\u0141\u015A-\u0167\u2018\u2019]/ig,
147 replaceAccent: function(q
) {
148 SearchHighlight
.matchAccent
.lastIndex
= 0;
149 if(SearchHighlight
.matchAccent
.test(q
)) {
150 for(var i
=0,l
=SearchHighlight
.regexAccent
.length
;i
<l
;i
++)
151 q
= q
.replace(SearchHighlight
.regexAccent
[i
][0],SearchHighlight
.regexAccent
[i
][1]);
155 escapeRegEx
: /((?:\\{2})*)([[\]{}*?|])/g, //the special chars . and + are already gone at this point because they are considered split chars
156 buildReplaceTools : function(query
) {
158 $.each(query
,function(i
,n
){
159 if(!SearchHighlight
.options
.min_length
|| n
.length
>=SearchHighlight
.options
.min_length
)
160 if(n
= SearchHighlight
.replaceAccent(n
).replace(SearchHighlight
.escapeRegEx
,"$1\\$2"))
164 if(!re
.length
) return;
165 regex
= re
.join("|");
166 switch(SearchHighlight
.options
.exact
) {
168 regex
= '\\b(?:'+regex
+')\\b';
171 regex
= '\\b\\w*('+regex
+')\\w*\\b';
174 SearchHighlight
.regex
= new RegExp(regex
, "gi");
176 $.each(re
,function(i
,n
){
177 SearchHighlight
.subs
[n
] = SearchHighlight
.options
.style_name
+
178 (SearchHighlight
.options
.style_name_suffix
?i
+1:'');
181 nosearch
: /s(?:cript|tyle)|textarea/i,
182 hiliteElement: function(el
) {
183 var opt
= SearchHighlight
.options
, elHighlight
, noHighlight
;
184 elHighlight
= opt
.highlight
?$(opt
.highlight
):$("body");
185 if(!elHighlight
.length
) elHighlight
= $("body");
186 noHighlight
= opt
.nohighlight
?$(opt
.nohighlight
):$([]);
188 elHighlight
.each(function(){
189 SearchHighlight
.hiliteTree(this,noHighlight
);
192 hiliteTree : function(el
,noHighlight
) {
193 if(noHighlight
.index(el
)!=-1) return;
194 var matchIndex
= SearchHighlight
.options
.exact
=="whole"?1:0;
195 for(var startIndex
=0,endIndex
=el
.childNodes
.length
;startIndex
<endIndex
;startIndex
++) {
196 var item
= el
.childNodes
[startIndex
];
197 if ( item
.nodeType
!= 8 ) {//comment node
199 if(item
.nodeType
==3) {
200 var text
= item
.data
, textNoAcc
= SearchHighlight
.replaceAccent(text
);
201 var newtext
="",match
,index
=0;
202 SearchHighlight
.regex
.lastIndex
= 0;
203 while(match
= SearchHighlight
.regex
.exec(textNoAcc
)) {
204 newtext
+= SearchHighlight
.fixTags(text
.substr(index
,match
.index
-index
))+'<'+SearchHighlight
.options
.tag_name
+' class="'+
205 SearchHighlight
.subs
[match
[matchIndex
].toLowerCase()]+'">'+SearchHighlight
.fixTags(text
.substr(match
.index
,match
[0].length
))+"</"+SearchHighlight
.options
.tag_name
+">";
206 index
= match
.index
+match
[0].length
;
209 //add the last part of the text
210 newtext
+= SearchHighlight
.fixTags(text
.substring(index
));
211 var repl
= $.merge([],$("<"+SearchHighlight
.options
.tag_name
+">"+newtext
+"</"+SearchHighlight
.options
.tag_name
+">")[0].childNodes
);
212 endIndex
+= repl
.length
-1;
213 startIndex
+= repl
.length
-1;
214 $(item
).before(repl
).remove();
217 if(item
.nodeType
==1 && item
.nodeName
.search(SearchHighlight
.nosearch
)==-1)
218 SearchHighlight
.hiliteTree(item
,noHighlight
);
223 fixTags : function(text
) {
224 return text
.replace("<","<").replace(">",">");