uni

Thing1's amazing uni repo
Log | Files | Refs | Submodules

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, "&lt;").replace(/>/g, "&gt;");
     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 });