search.js (20169B)
1 /* 2 * Copyright (c) 2015, 2025, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/ 6 */ 7 "use strict"; 8 const messages = { 9 enterTerm: "Enter a search term", 10 noResult: "No results found", 11 oneResult: "Found one result", 12 manyResults: "Found {0} results", 13 loading: "Loading search index...", 14 searching: "Searching...", 15 redirecting: "Redirecting to first result...", 16 } 17 const categories = { 18 modules: "Modules", 19 packages: "Packages", 20 types: "Classes and Interfaces", 21 members: "Members", 22 searchTags: "Search Tags" 23 }; 24 // Localized element descriptors must match values in enum IndexItem.Kind. 25 const itemDesc = [ 26 // Members 27 ["Enum constant in {0}"], 28 ["Variable in {0}"], 29 ["Static variable in {0}"], 30 ["Constructor for {0}"], 31 ["Element in {0}"], 32 ["Method in {0}"], 33 ["Static method in {0}"], 34 ["Record component of {0}"], 35 // Types in upper and lower case 36 ["Annotation Interface", "annotation interface"], 37 ["Enum Class", "enum class"], 38 ["Interface", "interface"], 39 ["Record Class", "record class"], 40 ["Class", "class"], 41 ["Exception Class", "exception class"], 42 // Tags 43 ["Search tag in {0}"], 44 ["System property in {0}"], 45 ["Section in {0}"], 46 ["External specification in {0}"], 47 // Other 48 ["Summary Page"], 49 ]; 50 const mbrDesc = "Member"; 51 const clsDesc = "Class" 52 const pkgDesc = "Package"; 53 const mdlDesc = "Module"; 54 const pkgDescLower = "package"; 55 const mdlDescLower = "module"; 56 const tagDesc = "Search Tag"; 57 const inDesc = "{0} in {1}"; 58 const descDesc = "Description"; 59 const linkLabel = "Go to search page"; 60 const NO_MATCH = {}; 61 const MAX_RESULTS = 300; 62 const UNICODE_LETTER = 0; 63 const UNICODE_DIGIT = 1; 64 const UNICODE_OTHER = 2; 65 function checkUnnamed(name, separator) { 66 return name === "<Unnamed>" || !name ? "" : name + separator; 67 } 68 function escapeHtml(str) { 69 return str.replace(/</g, "<").replace(/>/g, ">"); 70 } 71 function getHighlightedText(str, boundaries, from, to) { 72 var start = from; 73 var text = ""; 74 for (var i = 0; i < boundaries.length; i += 2) { 75 var b0 = boundaries[i]; 76 var b1 = boundaries[i + 1]; 77 if (b0 >= to || b1 <= from) { 78 continue; 79 } 80 text += escapeHtml(str.slice(start, Math.max(start, b0))); 81 text += "<span class='result-highlight'>"; 82 text += escapeHtml(str.slice(Math.max(start, b0), Math.min(to, b1))); 83 text += "</span>"; 84 start = Math.min(to, b1); 85 } 86 text += escapeHtml(str.slice(start, to)); 87 return text; 88 } 89 function getURLPrefix(item, category) { 90 var urlPrefix = ""; 91 var slash = "/"; 92 if (category === "modules") { 93 return item.l + slash; 94 } else if (category === "packages" && item.m) { 95 return item.m + slash; 96 } else if (category === "types" || category === "members") { 97 if (item.m) { 98 urlPrefix = item.m + slash; 99 } else { 100 $.each(packageSearchIndex, function(index, it) { 101 if (it.m && item.p === it.l) { 102 urlPrefix = it.m + slash; 103 item.m = it.m; 104 return false; 105 } 106 }); 107 } 108 } 109 return urlPrefix; 110 } 111 function getURL(item, category) { 112 if (item.url) { 113 return item.url; 114 } 115 var url = getURLPrefix(item, category); 116 if (category === "modules") { 117 url += "module-summary.html"; 118 } else if (category === "packages") { 119 if (item.u) { 120 url = item.u; 121 } else { 122 url += item.l.replace(/\./g, '/') + "/package-summary.html"; 123 } 124 } else if (category === "types") { 125 if (item.u) { 126 url = item.u; 127 } else { 128 url += checkUnnamed(item.p, "/").replace(/\./g, '/') + item.l + ".html"; 129 } 130 } else if (category === "members") { 131 url += checkUnnamed(item.p, "/").replace(/\./g, '/') + item.c + ".html" + "#"; 132 if (item.u) { 133 url += item.u; 134 } else { 135 url += item.l; 136 } 137 } else if (category === "searchTags") { 138 url += item.u; 139 } 140 item.url = url; 141 return url; 142 } 143 function createMatcher(term, camelCase) { 144 if (camelCase && !isUpperCase(term)) { 145 return null; // no need for camel-case matcher for lower case query 146 } 147 var pattern = ""; 148 var upperCase = []; 149 term.trim().split(/\s+/).forEach(function(w, index, array) { 150 var tokens = w.split(/(?=[\p{Lu},.()<>?[\/])/u); 151 for (var i = 0; i < tokens.length; i++) { 152 var s = tokens[i]; 153 // ',' and '?' are the only delimiters commonly followed by space in java signatures 154 pattern += "(" + escapeUnicodeRegex(s).replace(/[,?]/g, "$&\\s*?") + ")"; 155 upperCase.push(false); 156 if (i === tokens.length - 1 && index < array.length - 1) { 157 // space in query string matches all delimiters 158 pattern += "(.*?)"; 159 upperCase.push(isUpperCase(s[0])); 160 } else { 161 if (!camelCase && isUpperCase(s) && s.length === 1) { 162 pattern += "()"; 163 } else { 164 pattern += "([\\p{L}\\p{Nd}\\p{Sc}<>?[\\]]*?)"; 165 } 166 upperCase.push(isUpperCase(s[0])); 167 } 168 } 169 }); 170 var re = new RegExp(pattern, camelCase ? "gu" : "gui"); 171 re.upperCase = upperCase; 172 return re; 173 } 174 // Unicode regular expressions do not allow certain characters to be escaped 175 function escapeUnicodeRegex(pattern) { 176 return pattern.replace(/[\[\]{}()*+?.\\^$|\s]/g, '\\$&'); 177 } 178 function findMatch(matcher, input, startOfName, endOfName, prefixLength) { 179 var from = startOfName; 180 matcher.lastIndex = from; 181 var match = matcher.exec(input); 182 // Expand search area until we get a valid result or reach the beginning of the string 183 while (!match || match.index + match[0].length < startOfName || endOfName < match.index) { 184 if (from === 0) { 185 return NO_MATCH; 186 } 187 from = input.lastIndexOf(".", from - 2) + 1; 188 matcher.lastIndex = from; 189 match = matcher.exec(input); 190 } 191 var boundaries = []; 192 var matchEnd = match.index + match[0].length; 193 var score = 5; 194 var start = match.index; 195 var prevEnd = -1; 196 for (var i = 1; i < match.length; i += 2) { 197 var charType = getCharType(input[start]); 198 // capturing groups come in pairs, match and non-match 199 boundaries.push(start, start + match[i].length); 200 var prevChar = input[start - 1] || ""; 201 var nextChar = input[start + 1] || ""; 202 // make sure group is anchored on a word boundary 203 if (start !== 0 && start !== startOfName) { 204 if (charType === UNICODE_DIGIT && getCharType(prevChar) === UNICODE_DIGIT) { 205 return NO_MATCH; // Numeric token must match at first digit 206 } else if (charType === UNICODE_LETTER && getCharType(prevChar) === UNICODE_LETTER) { 207 if (!isUpperCase(input[start]) || (!isLowerCase(prevChar) && !isLowerCase(nextChar))) { 208 // Not returning NO_MATCH below is to enable upper-case query strings 209 if (!matcher.upperCase[i] || start !== prevEnd) { 210 return NO_MATCH; 211 } else if (!isUpperCase(input[start])) { 212 score -= 1.0; 213 } 214 } 215 } 216 } 217 prevEnd = start + match[i].length; 218 start += match[i].length + match[i + 1].length; 219 220 // Lower score for unmatched parts between matches 221 if (match[i + 1]) { 222 score -= rateDistance(match[i + 1]); 223 } 224 } 225 226 // Lower score for unmatched leading part of name 227 if (startOfName < match.index) { 228 score -= rateDistance(input.substring(startOfName, match.index)); 229 } 230 // Favor child or parent variety depending on whether parent is included in search 231 var matchIncludesContaining = match.index < startOfName; 232 // Lower score for unmatched trailing part of name, but exclude member listings 233 if (matchEnd < endOfName && input[matchEnd - 1] !== ".") { 234 let factor = matchIncludesContaining ? 0.1 : 0.8; 235 score -= rateDistance(input.substring(matchEnd, endOfName)) * factor; 236 } 237 // Lower score for unmatched prefix in member class name 238 if (prefixLength < match.index && prefixLength < startOfName) { 239 let factor = matchIncludesContaining ? 0.8 : 0.4; 240 score -= rateDistance(input.substring(prefixLength, Math.min(match.index, startOfName))) * factor; 241 } 242 // Rank qualified names by package name 243 if (prefixLength > 0) { 244 score -= rateDistance(input.substring(0, prefixLength)) * 0.2; 245 } 246 // Reduce score of constructors in member listings 247 if (matchEnd === prefixLength) { 248 score -= 0.1; 249 } 250 251 return score > 0 ? { 252 input: input, 253 score: score, 254 boundaries: boundaries 255 } : NO_MATCH; 256 } 257 function isLetter(s) { 258 return /\p{L}/u.test(s); 259 } 260 function isUpperCase(s) { 261 return /\p{Lu}/u.test(s); 262 } 263 function isLowerCase(s) { 264 return /\p{Ll}/u.test(s); 265 } 266 function isDigit(s) { 267 return /\p{Nd}/u.test(s); 268 } 269 function getCharType(s) { 270 if (isLetter(s)) { 271 return UNICODE_LETTER; 272 } else if (isDigit(s)) { 273 return UNICODE_DIGIT; 274 } else { 275 return UNICODE_OTHER; 276 } 277 } 278 function rateDistance(str) { 279 // Rate distance of string by counting word boundaries and camel-case tokens 280 return !str ? 0 281 : (str.split(/\b|(?<=[\p{Ll}_])\p{Lu}/u).length * 0.1 282 + (isUpperCase(str[0]) ? 0.08 : 0)); 283 } 284 function doSearch(request, response) { 285 var term = request.term.trim(); 286 var maxResults = request.maxResults || MAX_RESULTS; 287 var module = checkUnnamed(request.module, "/"); 288 var matcher = { 289 plainMatcher: createMatcher(term, false), 290 camelCaseMatcher: createMatcher(term, true) 291 } 292 var indexLoaded = indexFilesLoaded(); 293 294 function getPrefix(item, category) { 295 switch (category) { 296 case "packages": 297 return checkUnnamed(item.m, "/"); 298 case "types": 299 case "members": 300 return checkUnnamed(item.p, "."); 301 default: 302 return ""; 303 } 304 } 305 function getClassPrefix(item, category) { 306 if (category === "members" && (!item.k || (item.k < 8 && item.k !== "3"))) { 307 return item.c + "."; 308 } 309 return ""; 310 } 311 function searchIndex(indexArray, category) { 312 var matches = []; 313 if (!indexArray) { 314 if (!indexLoaded) { 315 matches.push({ l: messages.loading, category: category }); 316 } 317 return matches; 318 } 319 $.each(indexArray, function (i, item) { 320 if (module) { 321 var modulePrefix = getURLPrefix(item, category) || item.u; 322 if (modulePrefix.indexOf("/") > -1 && !modulePrefix.startsWith(module)) { 323 return; 324 } 325 } 326 var prefix = getPrefix(item, category); 327 var classPrefix = getClassPrefix(item, category); 328 var simpleName = classPrefix + item.l; 329 if (item.d) { 330 simpleName += " - " + item.d; 331 } 332 var qualName = prefix + simpleName; 333 var startOfName = classPrefix.length + prefix.length; 334 var endOfName = category === "members" && qualName.indexOf("(", startOfName) > -1 335 ? qualName.indexOf("(", startOfName) : qualName.length; 336 var m = findMatch(matcher.plainMatcher, qualName, startOfName, endOfName, prefix.length); 337 if (m === NO_MATCH && matcher.camelCaseMatcher) { 338 m = findMatch(matcher.camelCaseMatcher, qualName, startOfName, endOfName, prefix.length); 339 } 340 if (m !== NO_MATCH) { 341 m.indexItem = item; 342 m.name = simpleName; 343 m.category = category; 344 if (m.boundaries[0] < prefix.length) { 345 m.name = qualName; 346 } else { 347 m.boundaries = m.boundaries.map(function(b) { 348 return b - prefix.length; 349 }); 350 } 351 // m.name = m.name + " " + m.score.toFixed(3); 352 matches.push(m); 353 } 354 return true; 355 }); 356 return matches.sort(function(e1, e2) { 357 return e2.score - e1.score 358 || (category !== "members" 359 ? e1.name.localeCompare(e2.name) : 0); 360 }).slice(0, maxResults); 361 } 362 363 var result = searchIndex(moduleSearchIndex, "modules") 364 .concat(searchIndex(packageSearchIndex, "packages")) 365 .concat(searchIndex(typeSearchIndex, "types")) 366 .concat(searchIndex(memberSearchIndex, "members")) 367 .concat(searchIndex(tagSearchIndex, "searchTags")); 368 369 if (!indexLoaded) { 370 updateSearchResults = function() { 371 doSearch(request, response); 372 } 373 } else { 374 updateSearchResults = function() {}; 375 } 376 response(result); 377 } 378 // JQuery search menu implementation 379 $.widget("custom.catcomplete", $.ui.autocomplete, { 380 _create: function() { 381 this._super(); 382 this.widget().menu("option", "items", "> .result-item"); 383 // workaround for search result scrolling 384 this.menu._scrollIntoView = function _scrollIntoView( item ) { 385 var borderTop, paddingTop, offset, scroll, elementHeight, itemHeight; 386 if ( this._hasScroll() ) { 387 borderTop = parseFloat( $.css( this.activeMenu[ 0 ], "borderTopWidth" ) ) || 0; 388 paddingTop = parseFloat( $.css( this.activeMenu[ 0 ], "paddingTop" ) ) || 0; 389 offset = item.offset().top - this.activeMenu.offset().top - borderTop - paddingTop; 390 scroll = this.activeMenu.scrollTop(); 391 elementHeight = this.activeMenu.height() - 26; 392 itemHeight = item.outerHeight(); 393 394 if ( offset < 0 ) { 395 this.activeMenu.scrollTop( scroll + offset ); 396 } else if ( offset + itemHeight > elementHeight ) { 397 this.activeMenu.scrollTop( scroll + offset - elementHeight + itemHeight ); 398 } 399 } 400 }; 401 }, 402 _renderMenu: function(ul, items) { 403 var currentCategory = ""; 404 var widget = this; 405 widget.menu.bindings = $(); 406 $.each(items, function(index, item) { 407 if (item.category && item.category !== currentCategory) { 408 ul.append("<li class='ui-autocomplete-category'>" + categories[item.category] + "</li>"); 409 currentCategory = item.category; 410 } 411 var li = widget._renderItemData(ul, item); 412 if (item.category) { 413 li.attr("aria-label", categories[item.category] + " : " + item.l); 414 } else { 415 li.attr("aria-label", item.l); 416 } 417 li.attr("class", "result-item"); 418 }); 419 ul.append("<li class='ui-static-link'><div><a href='" + pathtoroot + "search.html?q=" 420 + encodeURI(widget.term) + "'>" + linkLabel + "</a></div></li>"); 421 }, 422 _renderItem: function(ul, item) { 423 var label = getResultLabel(item); 424 var resultDesc = getResultDescription(item); 425 return $("<li/>") 426 .append($("<div/>") 427 .append($("<span/>").addClass("search-result-label").html(label)) 428 .append($("<span/>").addClass("search-result-desc").html(resultDesc))) 429 .appendTo(ul); 430 }, 431 _resizeMenu: function () { 432 var ul = this.menu.element; 433 var missing = 0; 434 ul.children().each((i, e) => { 435 if (e.hasChildNodes() && e.firstChild.hasChildNodes()) { 436 var label = e.firstChild.firstChild; 437 missing = Math.max(missing, label.scrollWidth - label.clientWidth); 438 } 439 }); 440 ul.outerWidth( Math.max( 441 ul.width("").outerWidth() + missing + 40, 442 this.element.outerWidth() 443 )); 444 } 445 }); 446 function getResultLabel(item) { 447 if (item.l) { 448 return item.l; 449 } 450 return getHighlightedText(item.name, item.boundaries, 0, item.name.length); 451 } 452 function getResultDescription(item) { 453 if (!item.indexItem) { 454 return ""; 455 } 456 var kind; 457 switch (item.category) { 458 case "members": 459 var typeName = checkUnnamed(item.indexItem.p, ".") + item.indexItem.c; 460 var typeDesc = getEnclosingTypeDesc(item.indexItem); 461 kind = itemDesc[item.indexItem.k || 5][0]; 462 return kind.replace("{0}", typeDesc + " " + typeName); 463 case "types": 464 var pkgName = checkUnnamed(item.indexItem.p, ""); 465 kind = itemDesc[item.indexItem.k || 12][0]; 466 if (!pkgName) { 467 // Handle "All Classes" summary page and unnamed package 468 return item.indexItem.k === "18" ? kind : kind + " " + item.indexItem.l; 469 } 470 return getEnclosingDescription(kind, pkgDescLower, pkgName); 471 case "packages": 472 if (item.indexItem.k === "18") { 473 return itemDesc[item.indexItem.k][0]; // "All Packages" summary page 474 } else if (!item.indexItem.m) { 475 return pkgDesc + " " + item.indexItem.l; 476 } 477 var mdlName = item.indexItem.m; 478 return getEnclosingDescription(pkgDesc, mdlDescLower, mdlName); 479 case "modules": 480 return mdlDesc + " " + item.indexItem.l; 481 case "searchTags": 482 if (item.indexItem) { 483 var holder = item.indexItem.h; 484 kind = itemDesc[item.indexItem.k || 14][0]; 485 return holder ? kind.replace("{0}", holder) : kind; 486 } 487 } 488 return ""; 489 } 490 function getEnclosingDescription(elem, desc, label) { 491 return inDesc.replace("{0}", elem).replace("{1}", desc + " " + label); 492 } 493 function getEnclosingTypeDesc(item) { 494 if (!item.typeDesc) { 495 $.each(typeSearchIndex, function(index, it) { 496 if (it.l === item.c && it.p === item.p && it.m === item.m) { 497 item.typeDesc = itemDesc[it.k || 12][1]; 498 return false; 499 } 500 }); 501 } 502 return item.typeDesc || ""; 503 } 504 $(function() { 505 var search = $("#search-input"); 506 var reset = $("#reset-search"); 507 search.catcomplete({ 508 minLength: 1, 509 delay: 200, 510 source: function(request, response) { 511 if (request.term.trim() === "") { 512 return this.close(); 513 } 514 // Prevent selection of item at current mouse position 515 this.menu.previousFilter = "_"; 516 this.menu.filterTimer = this.menu._delay(function() { 517 delete this.previousFilter; 518 }, 1000); 519 return doSearch(request, response); 520 }, 521 response: function(event, ui) { 522 if (!ui.content.length) { 523 ui.content.push({ l: messages.noResult }); 524 } 525 }, 526 autoFocus: true, 527 focus: function(event, ui) { 528 return false; 529 }, 530 position: { 531 collision: "flip" 532 }, 533 select: function(event, ui) { 534 if (ui.item.indexItem) { 535 var url = getURL(ui.item.indexItem, ui.item.category); 536 window.location.href = pathtoroot + url; 537 search.blur(); 538 } 539 } 540 }); 541 search.val(''); 542 search.on("input", () => reset.css("visibility", search.val() ? "visible" : "hidden")) 543 search.prop("disabled", false); 544 search.attr("autocapitalize", "off"); 545 reset.prop("disabled", false); 546 reset.click(function() { 547 search.val('').focus(); 548 }); 549 });