diff --git a/src/librustdoc/html/static/js/externs.js b/src/librustdoc/html/static/js/externs.js index 629f90728d2f6..0fe0fdadbd210 100644 --- a/src/librustdoc/html/static/js/externs.js +++ b/src/librustdoc/html/static/js/externs.js @@ -8,10 +8,34 @@ function initSearch(searchIndex){} /** * @typedef {{ - * raw: string, - * query: string, - * type: string, - * id: string, + * name: string, + * fullPath: Array, + * pathWithoutLast: Array, + * pathLast: string, + * generics: Array, + * }} + */ +var QueryElement; + +/** + * @typedef {{ + * pos: number, + * totalElems: number, + * typeFilter: (null|string), + * userQuery: string, + * }} + */ +var ParserState; + +/** + * @typedef {{ + * original: string, + * userQuery: string, + * typeFilter: number, + * elems: Array, + * args: Array, + * returned: Array, + * foundElems: number, * }} */ var ParsedQuery; @@ -30,3 +54,30 @@ var ParsedQuery; * }} */ var Row; + +/** + * @typedef {{ + * in_args: Array, + * returned: Array, + * others: Array, + * query: ParsedQuery, + * }} + */ +var ResultsTable; + +/** + * @typedef {{ + * desc: string, + * displayPath: string, + * fullPath: string, + * href: string, + * id: number, + * lev: number, + * name: string, + * normalizedName: string, + * parent: (Object|undefined), + * path: string, + * ty: number, + * }} + */ +var Results; diff --git a/src/librustdoc/html/static/js/search.js b/src/librustdoc/html/static/js/search.js index ab52304491a2b..0d4e0a0b3289d 100644 --- a/src/librustdoc/html/static/js/search.js +++ b/src/librustdoc/html/static/js/search.js @@ -61,15 +61,6 @@ function printTab(nb) { }); } -function removeEmptyStringsFromArray(x) { - for (var i = 0, len = x.length; i < len; ++i) { - if (x[i] === "") { - x.splice(i, 1); - i -= 1; - } - } -} - /** * A function to compute the Levenshtein distance between two strings * Licensed under the Creative Commons Attribution-ShareAlike 3.0 Unported @@ -133,11 +124,436 @@ window.initSearch = function(rawSearchIndex) { searchState.input.value = params.search || ""; } + function isWhitespace(c) { + return " \t\n\r".indexOf(c) !== -1; + } + + function isSpecialStartCharacter(c) { + return "<\"".indexOf(c) !== -1; + } + + function isEndCharacter(c) { + return ",>-".indexOf(c) !== -1; + } + + function isStopCharacter(c) { + return isWhitespace(c) || isEndCharacter(c); + } + + function isErrorCharacter(c) { + return "()".indexOf(c) !== -1; + } + + function itemTypeFromName(typename) { + for (var i = 0, len = itemTypes.length; i < len; ++i) { + if (itemTypes[i] === typename) { + return i; + } + } + + throw new Error("Unknown type filter `" + typename + "`"); + } + + /** + * If we encounter a `"`, then we try to extract the string from it until we find another `"`. + * + * This function will throw an error in the following cases: + * * There is already another string element. + * * We are parsing a generic argument. + * * There is more than one element. + * * There is no closing `"`. + * + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {boolean} isInGenerics + */ + function getStringElem(query, parserState, isInGenerics) { + if (isInGenerics) { + throw new Error("`\"` cannot be used in generics"); + } else if (query.literalSearch) { + throw new Error("Cannot have more than one literal search element"); + } else if (parserState.totalElems - parserState.genericsElems > 0) { + throw new Error("Cannot use literal search when there is more than one element"); + } + parserState.pos += 1; + var start = parserState.pos; + var end = getIdentEndPosition(parserState); + if (parserState.pos >= parserState.length) { + throw new Error("Unclosed `\"`"); + } else if (parserState.userQuery[end] !== "\"") { + throw new Error(`Unexpected \`${parserState.userQuery[end]}\` in a string element`); + } else if (start === end) { + throw new Error("Cannot have empty string element"); + } + // To skip the quote at the end. + parserState.pos += 1; + query.literalSearch = true; + } + + /** + * Returns `true` if the current parser position is starting with "::". + * + * @param {ParserState} parserState + * + * @return {boolean} + */ + function isPathStart(parserState) { + return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) == '::'; + } + + /** + * Returns `true` if the current parser position is starting with "->". + * + * @param {ParserState} parserState + * + * @return {boolean} + */ + function isReturnArrow(parserState) { + return parserState.userQuery.slice(parserState.pos, parserState.pos + 2) == '->'; + } + + /** + * Returns `true` if the given `c` character is valid for an ident. + * + * @param {string} c + * + * @return {boolean} + */ + function isIdentCharacter(c) { + return ( + c === '_' || + (c >= '0' && c <= '9') || + (c >= 'a' && c <= 'z') || + (c >= 'A' && c <= 'Z')); + } + + /** + * Returns `true` if the given `c` character is a separator. + * + * @param {string} c + * + * @return {boolean} + */ + function isSeparatorCharacter(c) { + return c === "," || isWhitespaceCharacter(c); + } + + /** + * Returns `true` if the given `c` character is a whitespace. + * + * @param {string} c + * + * @return {boolean} + */ + function isWhitespaceCharacter(c) { + return c === " " || c === "\t"; + } + + /** + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {string} name - Name of the query element. + * @param {Array} generics - List of generics of this query element. + * + * @return {QueryElement} - The newly created `QueryElement`. + */ + function createQueryElement(query, parserState, name, generics, isInGenerics) { + if (name === '*' || (name.length === 0 && generics.length === 0)) { + return; + } + if (query.literalSearch && parserState.totalElems - parserState.genericsElems > 0) { + throw new Error("You cannot have more than one element if you use quotes"); + } + var pathSegments = name.split("::"); + if (pathSegments.length > 1) { + for (var i = 0, len = pathSegments.length; i < len; ++i) { + var pathSegment = pathSegments[i]; + + if (pathSegment.length === 0) { + if (i === 0) { + throw new Error("Paths cannot start with `::`"); + } else if (i + 1 === len) { + throw new Error("Paths cannot end with `::`"); + } + throw new Error("Unexpected `::::`"); + } + } + } + // In case we only have something like `

`, there is no name. + if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === "")) { + throw new Error("Found generics without a path"); + } + parserState.totalElems += 1; + if (isInGenerics) { + parserState.genericsElems += 1; + } + return { + name: name, + fullPath: pathSegments, + pathWithoutLast: pathSegments.slice(0, pathSegments.length - 1), + pathLast: pathSegments[pathSegments.length - 1], + generics: generics, + }; + } + + /** + * This function goes through all characters until it reaches an invalid ident character or the + * end of the query. It returns the position of the last character of the ident. + * + * @param {ParserState} parserState + * + * @return {integer} + */ + function getIdentEndPosition(parserState) { + var end = parserState.pos; + while (parserState.pos < parserState.length) { + var c = parserState.userQuery[parserState.pos]; + if (!isIdentCharacter(c)) { + if (isErrorCharacter(c)) { + throw new Error(`Unexpected \`${c}\``); + } else if ( + isStopCharacter(c) || + isSpecialStartCharacter(c) || + isSeparatorCharacter(c)) + { + break; + } + // If we allow paths ("str::string" for example). + else if (c === ":") { + if (!isPathStart(parserState)) { + break; + } + // Skip current ":". + parserState.pos += 1; + } else { + throw new Error(`Unexpected \`${c}\``); + } + } + parserState.pos += 1; + end = parserState.pos; + } + return end; + } + + /** + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {Array} elems - This is where the new {QueryElement} will be added. + * @param {boolean} isInGenerics + */ + function getNextElem(query, parserState, elems, isInGenerics) { + var generics = []; + + var start = parserState.pos; + var end; + // We handle the strings on their own mostly to make code easier to follow. + if (parserState.userQuery[parserState.pos] === "\"") { + start += 1; + getStringElem(query, parserState, isInGenerics); + end = parserState.pos - 1; + } else { + end = getIdentEndPosition(parserState); + } + if (parserState.pos < parserState.length && + parserState.userQuery[parserState.pos] === "<") + { + if (isInGenerics) { + throw new Error("Unexpected `<` after `<`"); + } else if (start >= end) { + throw new Error("Found generics without a path"); + } + parserState.pos += 1; + getItemsBefore(query, parserState, generics, ">"); + } + if (start >= end && generics.length === 0) { + return; + } + elems.push( + createQueryElement( + query, + parserState, + parserState.userQuery.slice(start, end), + generics, + isInGenerics + ) + ); + } + + /** + * This function parses the next query element until it finds `endChar`, calling `getNextElem` + * to collect each element. + * + * If there is no `endChar`, this function will implicitly stop at the end without raising an + * error. + * + * @param {ParsedQuery} query + * @param {ParserState} parserState + * @param {Array} elems - This is where the new {QueryElement} will be added. + * @param {string} endChar - This function will stop when it'll encounter this + * character. + */ + function getItemsBefore(query, parserState, elems, endChar) { + var foundStopChar = true; + + while (parserState.pos < parserState.length) { + var c = parserState.userQuery[parserState.pos]; + if (c === endChar) { + break; + } else if (isSeparatorCharacter(c)) { + parserState.pos += 1; + foundStopChar = true; + continue; + } else if (c === ":" && isPathStart(parserState)) { + throw new Error("Unexpected `::`: paths cannot start with `::`"); + } else if (c === ":" || isEndCharacter(c)) { + var extra = ""; + if (endChar === ">") { + extra = "`<`"; + } else if (endChar === "") { + extra = "`->`"; + } + throw new Error("Unexpected `" + c + "` after " + extra); + } + if (!foundStopChar) { + if (endChar !== "") { + throw new Error(`Expected \`,\`, \` \` or \`${endChar}\`, found \`${c}\``); + } + throw new Error(`Expected \`,\` or \` \`, found \`${c}\``); + } + var posBefore = parserState.pos; + getNextElem(query, parserState, elems, endChar === ">"); + // This case can be encountered if `getNextElem` encounted a "stop character" right from + // the start. For example if you have `,,` or `<>`. In this case, we simply move up the + // current position to continue the parsing. + if (posBefore === parserState.pos) { + parserState.pos += 1; + } + foundStopChar = false; + } + // We are either at the end of the string or on the `endChar`` character, let's move forward + // in any case. + parserState.pos += 1; + } + + /** + * Checks that the type filter doesn't have unwanted characters like `<>` (which are ignored + * if empty). + * + * @param {ParserState} parserState + */ + function checkExtraTypeFilterCharacters(parserState) { + var query = parserState.userQuery; + + for (var pos = 0; pos < parserState.pos; ++pos) { + if (!isIdentCharacter(query[pos]) && !isWhitespaceCharacter(query[pos])) { + throw new Error(`Unexpected \`${query[pos]}\` in type filter`); + } + } + } + + /** + * Parses the provided `query` input to fill `parserState`. If it encounters an error while + * parsing `query`, it'll throw an error. + * + * @param {ParsedQuery} query + * @param {ParserState} parserState + */ + function parseInput(query, parserState) { + var c, before; + var foundStopChar = true; + + while (parserState.pos < parserState.length) { + c = parserState.userQuery[parserState.pos]; + if (isStopCharacter(c)) { + foundStopChar = true; + if (isSeparatorCharacter(c)) { + parserState.pos += 1; + continue; + } else if (c === "-" || c === ">") { + if (isReturnArrow(parserState)) { + break; + } + throw new Error(`Unexpected \`${c}\` (did you mean \`->\`?)`); + } + throw new Error(`Unexpected \`${c}\``); + } else if (c === ":" && !isPathStart(parserState)) { + if (parserState.typeFilter !== null) { + throw new Error("Unexpected `:`"); + } + if (query.elems.length === 0) { + throw new Error("Expected type filter before `:`"); + } else if (query.elems.length !== 1 || parserState.totalElems !== 1) { + throw new Error("Unexpected `:`"); + } else if (query.literalSearch) { + throw new Error("You cannot use quotes on type filter"); + } + checkExtraTypeFilterCharacters(parserState); + // The type filter doesn't count as an element since it's a modifier. + parserState.typeFilter = query.elems.pop().name; + parserState.pos += 1; + parserState.totalElems = 0; + query.literalSearch = false; + foundStopChar = true; + continue; + } + if (!foundStopChar) { + if (parserState.typeFilter !== null) { + throw new Error(`Expected \`,\`, \` \` or \`->\`, found \`${c}\``); + } + throw new Error(`Expected \`,\`, \` \`, \`:\` or \`->\`, found \`${c}\``); + } + before = query.elems.length; + getNextElem(query, parserState, query.elems, false); + if (query.elems.length === before) { + // Nothing was added, weird... Let's increase the position to not remain stuck. + parserState.pos += 1; + } + foundStopChar = false; + } + while (parserState.pos < parserState.length) { + c = parserState.userQuery[parserState.pos]; + if (isReturnArrow(parserState)) { + parserState.pos += 2; + // Get returned elements. + getItemsBefore(query, parserState, query.returned, ""); + // Nothing can come afterward! + if (query.returned.length === 0) { + throw new Error("Expected at least one item after `->`"); + } + break; + } else { + parserState.pos += 1; + } + } + } + + /** + * Takes the user search input and returns an empty `ParsedQuery`. + * + * @param {string} userQuery + * + * @return {ParsedQuery} + */ + function newParsedQuery(userQuery) { + return { + original: userQuery, + userQuery: userQuery.toLowerCase(), + typeFilter: NO_TYPE_FILTER, + elems: [], + returned: [], + // Total number of "top" elements (does not include generics). + foundElems: 0, + literalSearch: false, + error: null, + }; + } + /** * Build an URL with search parameters. * * @param {string} search - The current search being performed. * @param {string|null} filterCrates - The current filtering crate (if any). + * * @return {string} */ function buildUrl(search, filterCrates) { @@ -167,33 +583,139 @@ window.initSearch = function(rawSearchIndex) { } /** - * Executes the query and returns a list of results for each results tab. - * @param {Object} query - The user query - * @param {Array} searchWords - The list of search words to query against - * @param {string} [filterCrates] - Crate to search in - * @return {{ - * in_args: Array, - * returned: Array, - * others: Array - * }} + * Parses the query. + * + * The supported syntax by this parser is as follow: + * + * ident = *(ALPHA / DIGIT / "_") + * path = ident *(DOUBLE-COLON ident) + * arg = path [generics] + * arg-without-generic = path + * type-sep = COMMA/WS *(COMMA/WS) + * nonempty-arg-list = *(type-sep) arg *(type-sep arg) *(type-sep) + * nonempty-arg-list-without-generics = *(type-sep) arg-without-generic + * *(type-sep arg-without-generic) *(type-sep) + * generics = OPEN-ANGLE-BRACKET [ nonempty-arg-list-without-generics ] *(type-sep) + * CLOSE-ANGLE-BRACKET/EOF + * return-args = RETURN-ARROW *(type-sep) nonempty-arg-list + * + * exact-search = [type-filter *WS COLON] [ RETURN-ARROW ] *WS QUOTE ident QUOTE [ generics ] + * type-search = [type-filter *WS COLON] [ nonempty-arg-list ] [ return-args ] + * + * query = *WS (exact-search / type-search) *WS + * + * type-filter = ( + * "mod" / + * "externcrate" / + * "import" / + * "struct" / + * "enum" / + * "fn" / + * "type" / + * "static" / + * "trait" / + * "impl" / + * "tymethod" / + * "method" / + * "structfield" / + * "variant" / + * "macro" / + * "primitive" / + * "associatedtype" / + * "constant" / + * "associatedconstant" / + * "union" / + * "foreigntype" / + * "keyword" / + * "existential" / + * "attr" / + * "derive" / + * "traitalias") + * + * OPEN-ANGLE-BRACKET = "<" + * CLOSE-ANGLE-BRACKET = ">" + * COLON = ":" + * DOUBLE-COLON = "::" + * QUOTE = %x22 + * COMMA = "," + * RETURN-ARROW = "->" + * + * ALPHA = %x41-5A / %x61-7A ; A-Z / a-z + * DIGIT = %x30-39 + * WS = %x09 / " " + * + * @param {string} val - The user query + * + * @return {ParsedQuery} - The parsed query */ - function execQuery(query, searchWords, filterCrates) { - function itemTypeFromName(typename) { - for (var i = 0, len = itemTypes.length; i < len; ++i) { - if (itemTypes[i] === typename) { - return i; + function parseQuery(userQuery) { + userQuery = userQuery.trim(); + var parserState = { + length: userQuery.length, + pos: 0, + // Total number of elements (includes generics). + totalElems: 0, + genericsElems: 0, + typeFilter: null, + userQuery: userQuery.toLowerCase(), + }; + var query = newParsedQuery(userQuery); + + try { + parseInput(query, parserState); + if (parserState.typeFilter !== null) { + var typeFilter = parserState.typeFilter; + if (typeFilter === "const") { + typeFilter = "constant"; } + query.typeFilter = itemTypeFromName(typeFilter); } - return NO_TYPE_FILTER; + } catch (err) { + query = newParsedQuery(userQuery); + query.error = err.message; + query.typeFilter = -1; + return query; } - var valLower = query.query.toLowerCase(), - val = valLower, - typeFilter = itemTypeFromName(query.type), - results = {}, results_in_args = {}, results_returned = {}, - split = valLower.split("::"); + if (!query.literalSearch) { + // If there is more than one element in the query, we switch to literalSearch in any + // case. + query.literalSearch = parserState.totalElems > 1; + } + query.foundElems = query.elems.length + query.returned.length; + return query; + } - removeEmptyStringsFromArray(split); + /** + * Creates the query results. + * + * @param {Array} results_in_args + * @param {Array} results_returned + * @param {Array} results_in_args + * @param {ParsedQuery} parsedQuery + * + * @return {ResultsTable} + */ + function createQueryResults(results_in_args, results_returned, results_others, parsedQuery) { + return { + "in_args": results_in_args, + "returned": results_returned, + "others": results_others, + "query": parsedQuery, + }; + } + + /** + * Executes the parsed query and builds a {ResultsTable}. + * + * @param {ParsedQuery} parsedQuery - The parsed user query + * @param {Object} searchWords - The list of search words to query against + * @param {Object} [filterCrates] - Crate to search in if defined + * + * @return {ResultsTable} + */ + function execQuery(parsedQuery, searchWords, filterCrates) { + var results_others = {}, results_in_args = {}, results_returned = {}; function transformResults(results) { var duplicates = {}; @@ -227,6 +749,7 @@ window.initSearch = function(rawSearchIndex) { } function sortResults(results, isType) { + var userQuery = parsedQuery.userQuery; var ar = []; for (var entry in results) { if (hasOwnPropertyRustdoc(results, entry)) { @@ -246,8 +769,8 @@ window.initSearch = function(rawSearchIndex) { var a, b; // sort by exact match with regard to the last word (mismatch goes later) - a = (aaa.word !== val); - b = (bbb.word !== val); + a = (aaa.word !== userQuery); + b = (bbb.word !== userQuery); if (a !== b) { return a - b; } // Sort by non levenshtein results and then levenshtein results by the distance @@ -309,6 +832,12 @@ window.initSearch = function(rawSearchIndex) { return 0; }); + var nameSplit = null; + if (parsedQuery.elems.length === 1) { + var hasPath = typeof parsedQuery.elems[0].path === "undefined"; + nameSplit = hasPath ? null : parsedQuery.elems[0].path; + } + for (var i = 0, len = results.length; i < len; ++i) { result = results[i]; @@ -320,215 +849,222 @@ window.initSearch = function(rawSearchIndex) { path = result.item.path.toLowerCase(), parent = result.item.parent; - if (!isType && !validateResult(name, path, split, parent)) { + if (!isType && !validateResult(name, path, nameSplit, parent)) { result.id = -1; } } return transformResults(results); } - function extractGenerics(val) { - val = val.toLowerCase(); - if (val.indexOf("<") !== -1) { - var values = val.substring(val.indexOf("<") + 1, val.lastIndexOf(">")); - return { - name: val.substring(0, val.indexOf("<")), - generics: values.split(/\s*,\s*/), - }; + /** + * This function checks if the object (`row`) generics match the given type (`elem`) + * generics. If there are no generics on `row`, `defaultLev` is returned. + * + * @param {Row} row - The object to check. + * @param {QueryElement} elem - The element from the parsed query. + * @param {integer} defaultLev - This is the value to return in case there are no generics. + * + * @return {integer} - Returns the best match (if any) or `MAX_LEV_DISTANCE + 1`. + */ + function checkGenerics(row, elem, defaultLev) { + if (row.length <= GENERICS_DATA || row[GENERICS_DATA].length === 0) { + return elem.generics.length === 0 ? defaultLev : MAX_LEV_DISTANCE + 1; + } else if (row[GENERICS_DATA].length > 0 && row[GENERICS_DATA][0][NAME] === "") { + if (row.length > GENERICS_DATA) { + return checkGenerics(row[GENERICS_DATA][0], elem, defaultLev); + } + return elem.generics.length === 0 ? defaultLev : MAX_LEV_DISTANCE + 1; } - return { - name: val, - generics: [], - }; - } - - function checkGenerics(obj, val) { // The names match, but we need to be sure that all generics kinda // match as well. - var tmp_lev, elem_name; - if (val.generics.length > 0) { - if (obj.length > GENERICS_DATA && - obj[GENERICS_DATA].length >= val.generics.length) { - var elems = Object.create(null); - var elength = obj[GENERICS_DATA].length; - for (var x = 0; x < elength; ++x) { - if (!elems[obj[GENERICS_DATA][x][NAME]]) { - elems[obj[GENERICS_DATA][x][NAME]] = 0; + var elem_name; + if (elem.generics.length > 0 && row[GENERICS_DATA].length >= elem.generics.length) { + var elems = Object.create(null); + for (var x = 0, length = row[GENERICS_DATA].length; x < length; ++x) { + elem_name = row[GENERICS_DATA][x][NAME]; + if (elem_name === "") { + // Pure generic, needs to check into it. + if (checkGenerics( + row[GENERICS_DATA][x], elem, MAX_LEV_DISTANCE + 1) !== 0) { + return MAX_LEV_DISTANCE + 1; } - elems[obj[GENERICS_DATA][x][NAME]] += 1; + continue; + } + if (elems[elem_name] === undefined) { + elems[elem_name] = 0; } - var total = 0; - var done = 0; - // We need to find the type that matches the most to remove it in order - // to move forward. - var vlength = val.generics.length; - for (x = 0; x < vlength; ++x) { - var lev = MAX_LEV_DISTANCE + 1; - var firstGeneric = val.generics[x]; - var match = null; - if (elems[firstGeneric]) { - match = firstGeneric; - lev = 0; - } else { - for (elem_name in elems) { - tmp_lev = levenshtein(elem_name, firstGeneric); - if (tmp_lev < lev) { - lev = tmp_lev; - match = elem_name; - } + elems[elem_name] += 1; + } + // We need to find the type that matches the most to remove it in order + // to move forward. + for (x = 0, length = elem.generics.length; x < length; ++x) { + var generic = elem.generics[x]; + var match = null; + if (elems[generic.name]) { + match = generic.name; + } else { + for (elem_name in elems) { + if (!hasOwnPropertyRustdoc(elems, elem_name)) { + continue; } - } - if (match !== null) { - elems[match] -= 1; - if (elems[match] == 0) { - delete elems[match]; + if (elem_name === generic) { + match = elem_name; + break; } - total += lev; - done += 1; - } else { - return MAX_LEV_DISTANCE + 1; } } - return Math.ceil(total / done); + if (match === null) { + return MAX_LEV_DISTANCE + 1; + } + elems[match] -= 1; + if (elems[match] === 0) { + delete elems[match]; + } } + return 0; } return MAX_LEV_DISTANCE + 1; } /** - * This function checks if the object (`obj`) matches the given type (`val`) and its + * This function checks if the object (`row`) matches the given type (`elem`) and its + * generics (if any). + * + * @param {Row} row + * @param {QueryElement} elem - The element from the parsed query. + * + * @return {integer} - Returns a Levenshtein distance to the best match. + */ + function checkIfInGenerics(row, elem) { + var lev = MAX_LEV_DISTANCE + 1; + for (var x = 0, length = row[GENERICS_DATA].length; x < length && lev !== 0; ++x) { + lev = Math.min( + checkType(row[GENERICS_DATA][x], elem, true), + lev + ); + } + return lev; + } + + /** + * This function checks if the object (`row`) matches the given type (`elem`) and its * generics (if any). * - * @param {Object} obj - * @param {string} val + * @param {Row} row + * @param {QueryElement} elem - The element from the parsed query. * @param {boolean} literalSearch * * @return {integer} - Returns a Levenshtein distance to the best match. If there is * no match, returns `MAX_LEV_DISTANCE + 1`. */ - function checkType(obj, val, literalSearch) { - var lev_distance = MAX_LEV_DISTANCE + 1; - var tmp_lev = MAX_LEV_DISTANCE + 1; - var len, x, firstGeneric; - if (obj[NAME] === val.name) { - if (literalSearch) { - if (val.generics && val.generics.length !== 0) { - if (obj.length > GENERICS_DATA && - obj[GENERICS_DATA].length > 0) { - var elems = Object.create(null); - len = obj[GENERICS_DATA].length; - for (x = 0; x < len; ++x) { - if (!elems[obj[GENERICS_DATA][x][NAME]]) { - elems[obj[GENERICS_DATA][x][NAME]] = 0; - } - elems[obj[GENERICS_DATA][x][NAME]] += 1; - } + function checkType(row, elem, literalSearch) { + if (row[NAME].length === 0) { + // This is a pure "generic" search, no need to run other checks. + if (row.length > GENERICS_DATA) { + return checkIfInGenerics(row, elem); + } + return MAX_LEV_DISTANCE + 1; + } - len = val.generics.length; - for (x = 0; x < len; ++x) { - firstGeneric = val.generics[x]; - if (elems[firstGeneric]) { - elems[firstGeneric] -= 1; - } else { - // Something wasn't found and this is a literal search so - // abort and return a "failing" distance. - return MAX_LEV_DISTANCE + 1; - } - } - // Everything was found, success! + var lev = levenshtein(row[NAME], elem.name); + if (literalSearch) { + if (lev !== 0) { + // The name didn't match, let's try to check if the generics do. + if (elem.generics.length === 0) { + var checkGeneric = (row.length > GENERICS_DATA && + row[GENERICS_DATA].length > 0); + if (checkGeneric && row[GENERICS_DATA].findIndex(function(tmp_elem) { + return tmp_elem[NAME] === elem.name; + }) !== -1) { return 0; } - return MAX_LEV_DISTANCE + 1; } - return 0; - } else { - // If the type has generics but don't match, then it won't return at this point. - // Otherwise, `checkGenerics` will return 0 and it'll return. - if (obj.length > GENERICS_DATA && obj[GENERICS_DATA].length !== 0) { - tmp_lev = checkGenerics(obj, val); - if (tmp_lev <= MAX_LEV_DISTANCE) { - return tmp_lev; - } - } - } - } else if (literalSearch) { - var found = false; - if ((!val.generics || val.generics.length === 0) && - obj.length > GENERICS_DATA && obj[GENERICS_DATA].length > 0) { - found = obj[GENERICS_DATA].some( - function(gen) { - return gen[NAME] === val.name; - }); - } - return found ? 0 : MAX_LEV_DISTANCE + 1; - } - lev_distance = Math.min(levenshtein(obj[NAME], val.name), lev_distance); - if (lev_distance <= MAX_LEV_DISTANCE) { - // The generics didn't match but the name kinda did so we give it - // a levenshtein distance value that isn't *this* good so it goes - // into the search results but not too high. - lev_distance = Math.ceil((checkGenerics(obj, val) + lev_distance) / 2); - } - if (obj.length > GENERICS_DATA && obj[GENERICS_DATA].length > 0) { - // We can check if the type we're looking for is inside the generics! - var olength = obj[GENERICS_DATA].length; - for (x = 0; x < olength; ++x) { - tmp_lev = Math.min(levenshtein(obj[GENERICS_DATA][x][NAME], val.name), tmp_lev); + return MAX_LEV_DISTANCE + 1; + } else if (elem.generics.length > 0) { + return checkGenerics(row, elem, MAX_LEV_DISTANCE + 1); } - if (tmp_lev !== 0) { - // If we didn't find a good enough result, we go check inside the generics of - // the generics. - for (x = 0; x < olength && tmp_lev !== 0; ++x) { - tmp_lev = Math.min( - checkType(obj[GENERICS_DATA][x], val, literalSearch), - tmp_lev - ); + return 0; + } else if (row.length > GENERICS_DATA) { + if (elem.generics.length === 0) { + if (lev === 0) { + return 0; + } + // The name didn't match so we now check if the type we're looking for is inside + // the generics! + lev = checkIfInGenerics(row, elem); + // Now whatever happens, the returned distance is "less good" so we should mark + // it as such, and so we add 0.5 to the distance to make it "less good". + return lev + 0.5; + } else if (lev > MAX_LEV_DISTANCE) { + // So our item's name doesn't match at all and has generics. + // + // Maybe it's present in a sub generic? For example "f>>()", if we're + // looking for "B", we'll need to go down. + return checkIfInGenerics(row, elem); + } else { + // At this point, the name kinda match and we have generics to check, so + // let's go! + var tmp_lev = checkGenerics(row, elem, lev); + if (tmp_lev > MAX_LEV_DISTANCE) { + return MAX_LEV_DISTANCE + 1; } + // We compute the median value of both checks and return it. + return (tmp_lev + lev) / 2; } + } else if (elem.generics.length > 0) { + // In this case, we were expecting generics but there isn't so we simply reject this + // one. + return MAX_LEV_DISTANCE + 1; } - // Now whatever happens, the returned distance is "less good" so we should mark it - // as such, and so we add 1 to the distance to make it "less good". - return Math.min(lev_distance, tmp_lev) + 1; + // No generics on our query or on the target type so we can return without doing + // anything else. + return lev; } /** - * This function checks if the object (`obj`) has an argument with the given type (`val`). + * This function checks if the object (`row`) has an argument with the given type (`elem`). * - * @param {Object} obj - * @param {string} val - * @param {boolean} literalSearch + * @param {Row} row + * @param {QueryElement} elem - The element from the parsed query. * @param {integer} typeFilter * * @return {integer} - Returns a Levenshtein distance to the best match. If there is no * match, returns `MAX_LEV_DISTANCE + 1`. */ - function findArg(obj, val, literalSearch, typeFilter) { - var lev_distance = MAX_LEV_DISTANCE + 1; + function findArg(row, elem, typeFilter) { + var lev = MAX_LEV_DISTANCE + 1; - if (obj && obj.type && obj.type[INPUTS_DATA] && obj.type[INPUTS_DATA].length > 0) { - var length = obj.type[INPUTS_DATA].length; + if (row && row.type && row.type[INPUTS_DATA] && row.type[INPUTS_DATA].length > 0) { + var length = row.type[INPUTS_DATA].length; for (var i = 0; i < length; i++) { - var tmp = obj.type[INPUTS_DATA][i]; + var tmp = row.type[INPUTS_DATA][i]; if (!typePassesFilter(typeFilter, tmp[1])) { continue; } - tmp = checkType(tmp, val, literalSearch); - if (tmp === 0) { + lev = Math.min(lev, checkType(tmp, elem, parsedQuery.literalSearch)); + if (lev === 0) { return 0; - } else if (literalSearch) { - continue; } - lev_distance = Math.min(tmp, lev_distance); } } - return literalSearch ? MAX_LEV_DISTANCE + 1 : lev_distance; + return parsedQuery.literalSearch ? MAX_LEV_DISTANCE + 1 : lev; } - function checkReturned(obj, val, literalSearch, typeFilter) { - var lev_distance = MAX_LEV_DISTANCE + 1; + /** + * This function checks if the object (`row`) returns the given type (`elem`). + * + * @param {Row} row + * @param {QueryElement} elem - The element from the parsed query. + * @param {integer} typeFilter + * + * @return {integer} - Returns a Levenshtein distance to the best match. If there is no + * match, returns `MAX_LEV_DISTANCE + 1`. + */ + function checkReturned(row, elem, typeFilter) { + var lev = MAX_LEV_DISTANCE + 1; - if (obj && obj.type && obj.type.length > OUTPUT_DATA) { - var ret = obj.type[OUTPUT_DATA]; + if (row && row.type && row.type.length > OUTPUT_DATA) { + var ret = row.type[OUTPUT_DATA]; if (typeof ret[0] === "string") { ret = [ret]; } @@ -537,16 +1073,13 @@ window.initSearch = function(rawSearchIndex) { if (!typePassesFilter(typeFilter, tmp[1])) { continue; } - tmp = checkType(tmp, val, literalSearch); - if (tmp === 0) { + lev = Math.min(lev, checkType(tmp, elem, parsedQuery.literalSearch)); + if (lev === 0) { return 0; - } else if (literalSearch) { - continue; } - lev_distance = Math.min(tmp, lev_distance); } } - return literalSearch ? MAX_LEV_DISTANCE + 1 : lev_distance; + return parsedQuery.literalSearch ? MAX_LEV_DISTANCE + 1 : lev; } function checkPath(contains, lastElem, ty) { @@ -621,13 +1154,14 @@ window.initSearch = function(rawSearchIndex) { } function handleAliases(ret, query, filterCrates) { + var lowerQuery = query.toLowerCase(); // We separate aliases and crate aliases because we want to have current crate // aliases to be before the others in the displayed results. var aliases = []; var crateAliases = []; if (filterCrates !== null) { - if (ALIASES[filterCrates] && ALIASES[filterCrates][query.search]) { - var query_aliases = ALIASES[filterCrates][query.search]; + if (ALIASES[filterCrates] && ALIASES[filterCrates][lowerQuery]) { + var query_aliases = ALIASES[filterCrates][lowerQuery]; var len = query_aliases.length; for (var i = 0; i < len; ++i) { aliases.push(createAliasFromItem(searchIndex[query_aliases[i]])); @@ -635,9 +1169,9 @@ window.initSearch = function(rawSearchIndex) { } } else { Object.keys(ALIASES).forEach(function(crate) { - if (ALIASES[crate][query.search]) { + if (ALIASES[crate][lowerQuery]) { var pushTo = crate === window.currentCrate ? crateAliases : aliases; - var query_aliases = ALIASES[crate][query.search]; + var query_aliases = ALIASES[crate][lowerQuery]; var len = query_aliases.length; for (var i = 0; i < len; ++i) { pushTo.push(createAliasFromItem(searchIndex[query_aliases[i]])); @@ -658,7 +1192,7 @@ window.initSearch = function(rawSearchIndex) { aliases.sort(sortFunc); var pushFunc = function(alias) { - alias.alias = query.raw; + alias.alias = query; var res = buildHrefAndPath(alias); alias.displayPath = pathSplitter(res[0]); alias.fullPath = alias.displayPath + alias.name; @@ -674,208 +1208,237 @@ window.initSearch = function(rawSearchIndex) { } /** - * This function adds the given result into the provided `res` map if it matches the + * This function adds the given result into the provided `results` map if it matches the * following condition: * - * * If it is a "literal search" (`isExact`), then `lev` must be 0. + * * If it is a "literal search" (`parsedQuery.literalSearch`), then `lev` must be 0. * * If it is not a "literal search", `lev` must be <= `MAX_LEV_DISTANCE`. * - * The `res` map contains information which will be used to sort the search results: + * The `results` map contains information which will be used to sort the search results: * - * * `fullId` is a `string`` used as the key of the object we use for the `res` map. + * * `fullId` is a `string`` used as the key of the object we use for the `results` map. * * `id` is the index in both `searchWords` and `searchIndex` arrays for this element. * * `index` is an `integer`` used to sort by the position of the word in the item's name. * * `lev` is the main metric used to sort the search results. * - * @param {boolean} isExact - * @param {Object} res + * @param {Results} results * @param {string} fullId * @param {integer} id * @param {integer} index * @param {integer} lev */ - function addIntoResults(isExact, res, fullId, id, index, lev) { - if (lev === 0 || (!isExact && lev <= MAX_LEV_DISTANCE)) { - if (res[fullId] !== undefined) { - var result = res[fullId]; + function addIntoResults(results, fullId, id, index, lev) { + if (lev === 0 || (!parsedQuery.literalSearch && lev <= MAX_LEV_DISTANCE)) { + if (results[fullId] !== undefined) { + var result = results[fullId]; if (result.dontValidate || result.lev <= lev) { return; } } - res[fullId] = { + results[fullId] = { id: id, index: index, - dontValidate: isExact, + dontValidate: parsedQuery.literalSearch, lev: lev, }; } } - // quoted values mean literal search - var nSearchWords = searchWords.length; - var i, it; - var ty; - var fullId; - var returned; - var in_args; - var len; - if ((val.charAt(0) === "\"" || val.charAt(0) === "'") && - val.charAt(val.length - 1) === val.charAt(0)) - { - val = extractGenerics(val.substr(1, val.length - 2)); - for (i = 0; i < nSearchWords; ++i) { - if (filterCrates !== null && searchIndex[i].crate !== filterCrates) { - continue; + /** + * This function is called in case the query is only one element (with or without generics). + * This element will be compared to arguments' and returned values' items and also to items. + * + * Other important thing to note: since there is only one element, we use levenshtein + * distance for name comparisons. + * + * @param {Row} row + * @param {integer} pos - Position in the `searchIndex`. + * @param {QueryElement} elem - The element from the parsed query. + * @param {Results} results_others - Unqualified results (not in arguments nor in + * returned values). + * @param {Results} results_in_args - Matching arguments results. + * @param {Results} results_returned - Matching returned arguments results. + */ + function handleSingleArg( + row, + pos, + elem, + results_others, + results_in_args, + results_returned + ) { + if (!row || (filterCrates !== null && row.crate !== filterCrates)) { + return; + } + var lev, lev_add = 0, index = -1; + var fullId = row.id; + + var in_args = findArg(row, elem, parsedQuery.typeFilter); + var returned = checkReturned(row, elem, parsedQuery.typeFilter); + + addIntoResults(results_in_args, fullId, pos, index, in_args); + addIntoResults(results_returned, fullId, pos, index, returned); + + if (!typePassesFilter(parsedQuery.typeFilter, row.ty)) { + return; + } + var searchWord = searchWords[pos]; + + if (parsedQuery.literalSearch) { + if (searchWord === elem.name) { + addIntoResults(results_others, fullId, pos, -1, 0); } - in_args = findArg(searchIndex[i], val, true, typeFilter); - returned = checkReturned(searchIndex[i], val, true, typeFilter); - ty = searchIndex[i]; - fullId = ty.id; - - if (searchWords[i] === val.name - && typePassesFilter(typeFilter, searchIndex[i].ty)) { - addIntoResults(true, results, fullId, i, -1, 0); + return; + } + + // No need to check anything else if it's a "pure" generics search. + if (elem.name.length === 0) { + if (row.type !== null) { + lev = checkGenerics(row.type, elem, MAX_LEV_DISTANCE + 1); + addIntoResults(results_others, fullId, pos, index, lev); } - addIntoResults(true, results_in_args, fullId, i, -1, in_args); - addIntoResults(true, results_returned, fullId, i, -1, returned); - } - query.inputs = [val]; - query.output = val; - query.search = val; - // searching by type - } else if (val.search("->") > -1) { - var trimmer = function(s) { return s.trim(); }; - var parts = val.split("->").map(trimmer); - var input = parts[0]; - // sort inputs so that order does not matter - var inputs = input.split(",").map(trimmer).sort(); - for (i = 0, len = inputs.length; i < len; ++i) { - inputs[i] = extractGenerics(inputs[i]); - } - var output = extractGenerics(parts[1]); - - for (i = 0; i < nSearchWords; ++i) { - if (filterCrates !== null && searchIndex[i].crate !== filterCrates) { - continue; + return; + } + + if (elem.fullPath.length > 1) { + lev = checkPath(elem.pathWithoutLast, elem.pathLast, row); + if (lev > MAX_LEV_DISTANCE || (parsedQuery.literalSearch && lev !== 0)) { + return; + } else if (lev > 0) { + lev_add = lev / 10; } - var type = searchIndex[i].type; - ty = searchIndex[i]; - if (!type) { - continue; + } + + if (searchWord.indexOf(elem.pathLast) > -1 || + row.normalizedName.indexOf(elem.pathLast) > -1) + { + // filter type: ... queries + if (!results_others[fullId] !== undefined) { + index = row.normalizedName.indexOf(elem.pathLast); } - fullId = ty.id; + } + lev = levenshtein(searchWord, elem.pathLast); + lev += lev_add; + if (lev > 0 && elem.pathLast.length > 2 && searchWord.indexOf(elem.pathLast) > -1) + { + if (elem.pathLast.length < 6) { + lev = 1; + } else { + lev = 0; + } + } + if (lev > MAX_LEV_DISTANCE) { + return; + } else if (index !== -1 && elem.fullPath.length < 2) { + lev -= 1; + } + if (lev < 0) { + lev = 0; + } + addIntoResults(results_others, fullId, pos, index, lev); + } - returned = checkReturned(ty, output, true, NO_TYPE_FILTER); - if (output.name === "*" || returned === 0) { - in_args = false; - var is_module = false; + /** + * This function is called in case the query has more than one element. In this case, it'll + * try to match the items which validates all the elements. For `aa -> bb` will look for + * functions which have a parameter `aa` and has `bb` in its returned values. + * + * @param {Row} row + * @param {integer} pos - Position in the `searchIndex`. + * @param {Object} results + */ + function handleArgs(row, pos, results) { + if (!row || (filterCrates !== null && row.crate !== filterCrates)) { + return; + } - if (input === "*") { - is_module = true; + var totalLev = 0; + var nbLev = 0; + var lev; + + // If the result is too "bad", we return false and it ends this search. + function checkArgs(elems, callback) { + for (var i = 0, len = elems.length; i < len; ++i) { + var elem = elems[i]; + // There is more than one parameter to the query so all checks should be "exact" + lev = callback(row, elem, NO_TYPE_FILTER); + if (lev <= 1) { + nbLev += 1; + totalLev += lev; } else { - var firstNonZeroDistance = 0; - for (it = 0, len = inputs.length; it < len; it++) { - var distance = checkType(type, inputs[it], true); - if (distance > 0) { - firstNonZeroDistance = distance; - break; - } - } - in_args = firstNonZeroDistance; - } - addIntoResults(true, results_in_args, fullId, i, -1, in_args); - addIntoResults(true, results_returned, fullId, i, -1, returned); - if (is_module) { - addIntoResults(true, results, fullId, i, -1, 0); + return false; } } + return true; + } + if (!checkArgs(parsedQuery.elems, findArg)) { + return; + } + if (!checkArgs(parsedQuery.returned, checkReturned)) { + return; } - query.inputs = inputs.map(function(input) { - return input.name; - }); - query.output = output.name; - } else { - query.inputs = [val]; - query.output = val; - query.search = val; - // gather matching search results up to a certain maximum - val = val.replace(/_/g, ""); - - var valGenerics = extractGenerics(val); - - var paths = valLower.split("::"); - removeEmptyStringsFromArray(paths); - val = paths[paths.length - 1]; - var contains = paths.slice(0, paths.length > 1 ? paths.length - 1 : 1); - - var lev, j; - for (j = 0; j < nSearchWords; ++j) { - ty = searchIndex[j]; - if (!ty || (filterCrates !== null && ty.crate !== filterCrates)) { - continue; - } - var lev_add = 0; - if (paths.length > 1) { - lev = checkPath(contains, paths[paths.length - 1], ty); - if (lev > MAX_LEV_DISTANCE) { - continue; - } else if (lev > 0) { - lev_add = lev / 10; - } - } - returned = MAX_LEV_DISTANCE + 1; - in_args = MAX_LEV_DISTANCE + 1; - var index = -1; - // we want lev results to go lower than others - lev = MAX_LEV_DISTANCE + 1; - fullId = ty.id; + if (nbLev === 0) { + return; + } + lev = Math.round(totalLev / nbLev); + addIntoResults(results, row.id, pos, 0, lev); + } - if (searchWords[j].indexOf(split[i]) > -1 || - searchWords[j].indexOf(val) > -1 || - ty.normalizedName.indexOf(val) > -1) - { - // filter type: ... queries - if (typePassesFilter(typeFilter, ty.ty) && results[fullId] === undefined) { - index = ty.normalizedName.indexOf(val); + function innerRunQuery() { + var elem, i, nSearchWords, in_returned, row; + + if (parsedQuery.foundElems === 1) { + if (parsedQuery.elems.length === 1) { + elem = parsedQuery.elems[0]; + for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { + // It means we want to check for this element everywhere (in names, args and + // returned). + handleSingleArg( + searchIndex[i], + i, + elem, + results_others, + results_in_args, + results_returned + ); } - } - if ((lev = levenshtein(searchWords[j], val)) <= MAX_LEV_DISTANCE) { - if (typePassesFilter(typeFilter, ty.ty)) { - lev += 1; - } else { - lev = MAX_LEV_DISTANCE + 1; + } else if (parsedQuery.returned.length === 1) { + // We received one returned argument to check, so looking into returned values. + elem = parsedQuery.returned[0]; + for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { + row = searchIndex[i]; + in_returned = checkReturned(row, elem, parsedQuery.typeFilter); + addIntoResults(results_returned, row.id, i, -1, in_returned); } } - in_args = findArg(ty, valGenerics, false, typeFilter); - returned = checkReturned(ty, valGenerics, false, typeFilter); - - lev += lev_add; - if (lev > 0 && val.length > 3 && searchWords[j].indexOf(val) > -1) { - if (val.length < 6) { - lev -= 1; - } else { - lev = 0; - } + } else if (parsedQuery.foundElems > 0) { + var container = results_others; + // In the special case where only a "returned" information is available, we want to + // put the information into the "results_returned" dict. + if (parsedQuery.returned.length !== 0 && parsedQuery.elems.length === 0) { + container = results_returned; } - addIntoResults(false, results_in_args, fullId, j, index, in_args); - addIntoResults(false, results_returned, fullId, j, index, returned); - if (typePassesFilter(typeFilter, ty.ty) && - (index !== -1 || lev <= MAX_LEV_DISTANCE)) { - if (index !== -1 && paths.length < 2) { - lev = 0; - } - addIntoResults(false, results, fullId, j, index, lev); + for (i = 0, nSearchWords = searchWords.length; i < nSearchWords; ++i) { + handleArgs(searchIndex[i], i, container); } } } - var ret = { - "in_args": sortResults(results_in_args, true), - "returned": sortResults(results_returned, true), - "others": sortResults(results, false), - }; - handleAliases(ret, query, filterCrates); + if (parsedQuery.error === null) { + innerRunQuery(); + } + + var ret = createQueryResults( + sortResults(results_in_args, true), + sortResults(results_returned, true), + sortResults(results_others, false), + parsedQuery); + handleAliases(ret, parsedQuery.original.replace(/"/g, ""), filterCrates); + if (parsedQuery.error !== null && ret.others.length !== 0) { + // It means some doc aliases were found so let's "remove" the error! + ret.query.error = null; + } return ret; } @@ -892,9 +1455,13 @@ window.initSearch = function(rawSearchIndex) { * @param {string} path - The path of the result * @param {string} keys - The keys to be used (["file", "open"]) * @param {Object} parent - The parent of the result + * * @return {boolean} - Whether the result is valid or not */ function validateResult(name, path, keys, parent) { + if (!keys || !keys.length) { + return true; + } for (var i = 0, len = keys.length; i < len; ++i) { // each check is for validation so we negate the conditions and invalidate if (!( @@ -913,30 +1480,6 @@ window.initSearch = function(rawSearchIndex) { return true; } - /** - * Parse a string into a query object. - * - * @param {string} raw - The text that the user typed. - * @returns {ParsedQuery} - */ - function getQuery(raw) { - var matches, type = "", query; - query = raw; - - matches = query.match(/^(fn|mod|struct|enum|trait|type|const|macro)\s*:\s*/i); - if (matches) { - type = matches[1].replace(/^const$/, "constant"); - query = query.substring(matches[0].length); - } - - return { - raw: raw, - query: query, - type: type, - id: query + type - }; - } - function nextTab(direction) { var next = (searchState.currentTab + direction + 3) % searchState.focusedByTab.length; searchState.focusedByTab[searchState.currentTab] = document.activeElement; @@ -1088,11 +1631,11 @@ window.initSearch = function(rawSearchIndex) { link.appendChild(wrapper); output.appendChild(link); }); - } else { + } else if (query.error === null) { output.className = "search-failed" + extraClass; output.innerHTML = "No results :(
" + "Try on DuckDuckGo?

" + "Or try looking in one of these: