From cf9284536086df6b34f8e5b260e9984c0941e1c0 Mon Sep 17 00:00:00 2001 From: Mukher Date: Thu, 13 Aug 2026 08:20:46 +0500 Subject: [PATCH 1/4] Escape search query values and resolve typed workspace names Two mechanisms handled query values and each dropped something. The grammar had no way to write a quotation mark, so a workspace or room called `Acme "US",Inc` could not be expressed and the previous fix silently deleted the quotes. And a name that was typed rather than picked was never mapped to an ID, so the literal text went to the server and the search came back empty. Give the grammar an escape character. A backslash only escapes what the parser would otherwise consume, so a value already stored in a saved search or a shared URL keeps the meaning it has today, including one that contains a lone backslash. Both parsers are regenerated from the shared rules. Ranges now carry the offset and width of the text each part actually matched. Walking the parsed values was only correct while a value and its source were the same length, which escaping breaks, and those ranges position both the substitutions and the live markdown highlighting. Write the escapes in sanitizeSearchValue, and resolve a typed workspace name to its ID when exactly one workspace matches. Names are not unique, so an ambiguous one is left alone rather than guessing. With any value now expressible, the quote stripping workarounds are gone. A comma inside quotes needs no escape, so a value that held one still serializes byte for byte as before and keeps its query hash. Only a value containing a quote or a backslash serializes differently, and those are the values that do not survive a round trip today. --- .../Search/SearchAutocompleteList.tsx | 6 +- .../Search/SearchRouter/SearchRouterUtils.ts | 22 +- src/libs/SearchAutocompleteUtils.ts | 17 +- src/libs/SearchParser/autocompleteParser.js | 881 ++++++++++-------- .../SearchParser/autocompleteParser.peggy | 28 +- src/libs/SearchParser/baseRules.peggy | 11 +- src/libs/SearchParser/searchParser.js | 439 +++++---- src/libs/SearchQueryUtils.ts | 46 +- tests/unit/Search/SearchQueryUtilsTest.ts | 50 +- tests/unit/SearchAutocompleteParserTest.ts | 32 + tests/unit/SearchAutocompleteUtilsTest.ts | 32 +- tests/unit/SearchRouterUtilsTest.ts | 9 +- 12 files changed, 875 insertions(+), 698 deletions(-) diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 812bd1f1ff2c..62ff1b234535 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -29,7 +29,6 @@ import {getAllTaxRates} from '@libs/PolicyUtils'; import {getReportAction} from '@libs/ReportActionsUtils'; import type {OptionData} from '@libs/ReportUtils'; import {formatReportLastMessageText, getReportOrDraftReport, getReportSubtitlePrefix} from '@libs/ReportUtils'; -import {getParsableSearchValue} from '@libs/SearchAutocompleteUtils'; import {buildSearchQueryJSON, buildUserReadableQueryString, getQueryWithoutFilters, shouldHighlight} from '@libs/SearchQueryUtils'; import StringUtils from '@libs/StringUtils'; import {cancelSpan, endSpan, getSpan} from '@libs/telemetry/activeSpans'; @@ -592,12 +591,11 @@ function SearchAutocompleteList({ if (autocompleteSuggestions.length > 0) { const autocompleteData: AutocompleteListItem[] = autocompleteSuggestions.map(({filterKey, text, autocompleteID, mapKey, workspaceIcon}) => { - const value = mapKey && autocompleteID ? getParsableSearchValue(filterKey, text) : text; return { text: getAutocompleteDisplayText(filterKey, text), - mapKey: mapKey ? getSubstitutionMapKey(mapKey, value) : undefined, + mapKey: mapKey ? getSubstitutionMapKey(mapKey, text) : undefined, singleIcon: expensifyIcons.MagnifyingGlass, - searchQuery: value, + searchQuery: text, autocompleteID, keyForList: autocompleteID ?? text, // in case we have a unique identifier then use it because text might not be unique searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION, diff --git a/src/components/Search/SearchRouter/SearchRouterUtils.ts b/src/components/Search/SearchRouter/SearchRouterUtils.ts index ad608cdfd7fb..91e903166381 100644 --- a/src/components/Search/SearchRouter/SearchRouterUtils.ts +++ b/src/components/Search/SearchRouter/SearchRouterUtils.ts @@ -1,6 +1,5 @@ import type {SearchQueryItem} from '@components/Search/SearchList/ListItem/SearchQueryListItem'; -import {getParsableSearchValue} from '@libs/SearchAutocompleteUtils'; import {getPolicyNameWithFallback, sanitizeSearchValue} from '@libs/SearchQueryUtils'; import type {ReportsSplitNavigatorParamList} from '@navigation/types'; @@ -59,16 +58,13 @@ function getContextualReportData(state: NavigationState | undefined): Contextual function getContextualSearchAutocompleteKey(item: SearchQueryItem, policies: OnyxCollection, reports?: OnyxCollection) { if (item.roomType === CONST.SEARCH.DATA_TYPES.INVOICE) { - return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.TO}:${getParsableSearchValue(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TO, item.searchQuery ?? '')}`; + return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.TO}:${item.searchQuery ?? ''}`; } if (item.roomType === CONST.SEARCH.DATA_TYPES.CHAT) { - return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.IN}:${getParsableSearchValue(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.IN, item.searchQuery ?? '')}`; + return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.IN}:${item.searchQuery ?? ''}`; } if (item.roomType === CONST.SEARCH.DATA_TYPES.EXPENSE) { - return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${getParsableSearchValue( - CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID, - item.policyID ? getPolicyNameWithFallback(item.policyID, policies, reports) : '', - )}`; + return `${CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID}:${item.policyID ? getPolicyNameWithFallback(item.policyID, policies, reports) : ''}`; } } @@ -79,25 +75,19 @@ function getContextualSearchQuery(item: SearchQueryItem, policies: OnyxCollectio switch (item.roomType) { case CONST.SEARCH.DATA_TYPES.EXPENSE: additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID}:${sanitizeSearchValue( - getParsableSearchValue(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID, item.policyID ? getPolicyNameWithFallback(item.policyID, policies, reports) : ''), + item.policyID ? getPolicyNameWithFallback(item.policyID, policies, reports) : '', true, )}`; break; case CONST.SEARCH.DATA_TYPES.INVOICE: additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID}:${item.policyID}`; if (item.autocompleteID) { - additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TO}:${sanitizeSearchValue( - getParsableSearchValue(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TO, item.searchQuery ?? ''), - true, - )}`; + additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TO}:${sanitizeSearchValue(item.searchQuery ?? '', true)}`; } break; case CONST.SEARCH.DATA_TYPES.CHAT: default: - additionalQuery = ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.IN}:${sanitizeSearchValue( - getParsableSearchValue(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.IN, item.searchQuery ?? ''), - true, - )}`; + additionalQuery = ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.IN}:${sanitizeSearchValue(item.searchQuery ?? '', true)}`; break; } return baseQuery + additionalQuery; diff --git a/src/libs/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index 57518b122205..8ff1f0652540 100644 --- a/src/libs/SearchAutocompleteUtils.ts +++ b/src/libs/SearchAutocompleteUtils.ts @@ -10,7 +10,7 @@ import type {SharedValue} from 'react-native-reanimated/lib/typescript/commonTyp import {getTagNamesFromTagsLists} from './PolicyUtils'; import {parse} from './SearchParser/autocompleteParser'; -import {getUserFriendlyValue, sanitizeSearchValue, stripSearchValueQuotes} from './SearchQueryUtils'; +import {getUserFriendlyValue} from './SearchQueryUtils'; /** * Parses given query using the autocomplete parser. @@ -25,20 +25,6 @@ function parseForAutocomplete(text: string) { } } -/** - * Returns a value that survives a round trip through the parser under the given filter key. Quotes are only dropped - * when the value cannot be read back as one value, because `from` and the other name filters carry them fine while - * `workspace` and `in` do not. Only safe for a value that is swapped for an ID before the query is sent. - */ -function getParsableSearchValue(filterKey: string, value: string) { - const ranges = parseForAutocomplete(`${filterKey}:${sanitizeSearchValue(value, true)}`)?.ranges ?? []; - if (ranges.length === 1 && ranges.at(0)?.value === value) { - return value; - } - - return stripSearchValueQuotes(value); -} - /** * Returns data for computing the `Tag` filter autocomplete list. */ @@ -349,7 +335,6 @@ function getTrimmedUserSearchQueryPreservingComma(textInputValue: string, fieldK export { getAutocompleteCategories, - getParsableSearchValue, getAutocompleteQueryWithComma, getAutocompleteRecentCategories, getAutocompleteRecentTags, diff --git a/src/libs/SearchParser/autocompleteParser.js b/src/libs/SearchParser/autocompleteParser.js index ff068a8f9b17..c819d2d2c864 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -296,23 +296,25 @@ function peg$parse(input, options) { var peg$c110 = ">"; var peg$c111 = "<="; var peg$c112 = "<"; - var peg$c113 = "\u201C"; - var peg$c114 = "\u201D"; - var peg$c115 = "\""; + var peg$c113 = "\\"; + var peg$c114 = "\u201C"; + var peg$c115 = "\u201D"; + var peg$c116 = "\""; var peg$r0 = /^[ \t\r\n\xA0,:=<>!]/; var peg$r1 = /^[:=]/; - var peg$r2 = /^[^ ,\t\n\r\xA0]/; - var peg$r3 = /^[ \t\r\n\xA0]/; - var peg$r4 = /^[^ ,"\u201D\u201C\t\n\r\xA0]/; - var peg$r5 = /^["\u201C-\u201D]/; - var peg$r6 = /^[^"\u201D\u201C\r\n]/; - var peg$r7 = /^[ \t\n\r\xA0a-zA-Z0-9]/; - var peg$r8 = /^[a-zA-Z0-9]/; - var peg$r9 = /^[ \t\n\r\xA0]/; - var peg$r10 = /^[ \t\n\r\xA0a-zA-Z]/; - var peg$r11 = /^[,]/; - var peg$r12 = /^[ \t\n\r\xA0,]/; + var peg$r2 = /^[,"\u201D\u201C\\]/; + var peg$r3 = /^[^ ,\t\n\r\xA0]/; + var peg$r4 = /^[ \t\r\n\xA0]/; + var peg$r5 = /^[^ ,"\u201D\u201C\t\n\r\xA0]/; + var peg$r6 = /^["\u201C-\u201D]/; + var peg$r7 = /^[^"\u201D\u201C\r\n]/; + var peg$r8 = /^[ \t\n\r\xA0a-zA-Z0-9]/; + var peg$r9 = /^[a-zA-Z0-9]/; + var peg$r10 = /^[ \t\n\r\xA0]/; + var peg$r11 = /^[ \t\n\r\xA0a-zA-Z]/; + var peg$r12 = /^[,]/; + var peg$r13 = /^[ \t\n\r\xA0,]/; var peg$e0 = peg$literalExpectation("-", false); var peg$e1 = peg$literalExpectation("report-field", true); @@ -432,23 +434,25 @@ function peg$parse(input, options) { var peg$e115 = peg$literalExpectation(">", false); var peg$e116 = peg$literalExpectation("<=", false); var peg$e117 = peg$literalExpectation("<", false); - var peg$e118 = peg$otherExpectation("word"); - var peg$e119 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e120 = peg$otherExpectation("whitespace"); - var peg$e121 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); - var peg$e122 = peg$otherExpectation("quote"); - var peg$e123 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e124 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); - var peg$e125 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); - var peg$e126 = peg$literalExpectation("\u201C", false); - var peg$e127 = peg$literalExpectation("\u201D", false); - var peg$e128 = peg$literalExpectation("\"", false); - var peg$e129 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e130 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e131 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); - var peg$e132 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); - var peg$e133 = peg$classExpectation([","], false, false); - var peg$e134 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ","], false, false); + var peg$e118 = peg$literalExpectation("\\", false); + var peg$e119 = peg$classExpectation([",", "\"", "\u201D", "\u201C", "\\"], false, false); + var peg$e120 = peg$otherExpectation("word"); + var peg$e121 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e122 = peg$otherExpectation("whitespace"); + var peg$e123 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); + var peg$e124 = peg$otherExpectation("quote"); + var peg$e125 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e126 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); + var peg$e127 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); + var peg$e128 = peg$literalExpectation("\u201C", false); + var peg$e129 = peg$literalExpectation("\u201D", false); + var peg$e130 = peg$literalExpectation("\"", false); + var peg$e131 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e132 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e133 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); + var peg$e134 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); + var peg$e135 = peg$classExpectation([","], false, false); + var peg$e136 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ","], false, false); var peg$f0 = function(ranges) { return { autocomplete, ranges }; }; var peg$f1 = function(filters) { return filters.filter(Boolean).flat(); }; @@ -490,146 +494,148 @@ function peg$parse(input, options) { nameOperator = (k === "from" || k === "to" || k === "payer" || k === "exporter" || k === "attendee" || k === "createdBy" || k === "assignee"); return k; }; - var peg$f6 = function(parts, empty) { + var peg$f6 = function(part) { + const value = Array.isArray(part) ? part.flat().filter(Boolean)[0] : part; + return {value, start: offset(), length: text().length}; + }; + var peg$f7 = function(parts, empty) { const ends = location(); - const value = parts.flat().filter(Boolean); // Filter out undefined values returned by the predicate - if (empty) { - value.push(""); - } - let count = ends.start.offset; const result = []; - value.forEach((filter) => { - let word = filter; + parts.filter((part) => Boolean(part.value)).forEach((part) => { + let word = part.value; if (word.startsWith('"') && word.endsWith('"') && word.length >= 2) { word = word.slice(1, -1); } result.push({ value: word, - start: count, - length: filter.length, + start: part.start, + length: part.length, }); - count += filter.length + 1; }); + if (empty) { + result.push({value: "", start: ends.end.offset, length: 0}); + } return result; }; - var peg$f7 = function() { return "date"; }; - var peg$f8 = function() { return "amount"; }; - var peg$f9 = function() { return "merchant"; }; - var peg$f10 = function() { return "description"; }; - var peg$f11 = function() { return "reportID"; }; - var peg$f12 = function() { return "keyword"; }; - var peg$f13 = function() { return "in"; }; - var peg$f14 = function() { return "currency"; }; - var peg$f15 = function() { return "groupCurrency"; }; - var peg$f16 = function() { return "tag"; }; - var peg$f17 = function() { return "category"; }; - var peg$f18 = function() { return "to"; }; - var peg$f19 = function() { return "exporter"; }; - var peg$f20 = function() { return "payer"; }; - var peg$f21 = function() { return "taxRate"; }; - var peg$f22 = function() { return "cardID"; }; - var peg$f23 = function() { return "bankAccount"; }; - var peg$f24 = function() { return "from"; }; - var peg$f25 = function() {return "attendee"}; - var peg$f26 = function() { return "expenseType"; }; - var peg$f27 = function() { return "receiptType"; }; - var peg$f28 = function() { return "withdrawalType"; }; - var peg$f29 = function() { return "withdrawalStatus"; }; - var peg$f30 = function() { return "paidStatus"; }; - var peg$f31 = function() { return "withdrawalID"; }; - var peg$f32 = function() { return "billable"; }; - var peg$f33 = function() { return "reimbursable"; }; - var peg$f34 = function() { return "type"; }; - var peg$f35 = function() { return "status"; }; - var peg$f36 = function() { return "sortBy"; }; - var peg$f37 = function() { return "sortOrder"; }; - var peg$f38 = function() { return "policyID"; }; - var peg$f39 = function() { return "submitted"; }; - var peg$f40 = function() { return "approved"; }; - var peg$f41 = function() { return "paid"; }; - var peg$f42 = function() { return "exported"; }; - var peg$f43 = function() { return "posted"; }; - var peg$f44 = function() { return "withdrawn"; }; - var peg$f45 = function() { return "groupBy"; }; - var peg$f46 = function() { return "limit"; }; - var peg$f47 = function() { return "feed"; }; - var peg$f48 = function() { return "title"; }; - var peg$f49 = function() { return "submitterUserID"; }; - var peg$f50 = function() { return "submitterPayrollID"; }; - var peg$f51 = function() { return "orderDealNumbers"; }; - var peg$f52 = function() { return "assignee"; }; - var peg$f53 = function() { return "createdBy"; }; - var peg$f54 = function() { return "action"; }; - var peg$f55 = function() {return "total"; }; - var peg$f56 = function() {return "has"; }; - var peg$f57 = function() {return "is"; }; - var peg$f58 = function() {return "purchaseAmount"}; - var peg$f59 = function() {return "purchaseCurrency"}; - var peg$f60 = function() { + var peg$f8 = function() { return "date"; }; + var peg$f9 = function() { return "amount"; }; + var peg$f10 = function() { return "merchant"; }; + var peg$f11 = function() { return "description"; }; + var peg$f12 = function() { return "reportID"; }; + var peg$f13 = function() { return "keyword"; }; + var peg$f14 = function() { return "in"; }; + var peg$f15 = function() { return "currency"; }; + var peg$f16 = function() { return "groupCurrency"; }; + var peg$f17 = function() { return "tag"; }; + var peg$f18 = function() { return "category"; }; + var peg$f19 = function() { return "to"; }; + var peg$f20 = function() { return "exporter"; }; + var peg$f21 = function() { return "payer"; }; + var peg$f22 = function() { return "taxRate"; }; + var peg$f23 = function() { return "cardID"; }; + var peg$f24 = function() { return "bankAccount"; }; + var peg$f25 = function() { return "from"; }; + var peg$f26 = function() {return "attendee"}; + var peg$f27 = function() { return "expenseType"; }; + var peg$f28 = function() { return "receiptType"; }; + var peg$f29 = function() { return "withdrawalType"; }; + var peg$f30 = function() { return "withdrawalStatus"; }; + var peg$f31 = function() { return "paidStatus"; }; + var peg$f32 = function() { return "withdrawalID"; }; + var peg$f33 = function() { return "billable"; }; + var peg$f34 = function() { return "reimbursable"; }; + var peg$f35 = function() { return "type"; }; + var peg$f36 = function() { return "status"; }; + var peg$f37 = function() { return "sortBy"; }; + var peg$f38 = function() { return "sortOrder"; }; + var peg$f39 = function() { return "policyID"; }; + var peg$f40 = function() { return "submitted"; }; + var peg$f41 = function() { return "approved"; }; + var peg$f42 = function() { return "paid"; }; + var peg$f43 = function() { return "exported"; }; + var peg$f44 = function() { return "posted"; }; + var peg$f45 = function() { return "withdrawn"; }; + var peg$f46 = function() { return "groupBy"; }; + var peg$f47 = function() { return "limit"; }; + var peg$f48 = function() { return "feed"; }; + var peg$f49 = function() { return "title"; }; + var peg$f50 = function() { return "submitterUserID"; }; + var peg$f51 = function() { return "submitterPayrollID"; }; + var peg$f52 = function() { return "orderDealNumbers"; }; + var peg$f53 = function() { return "assignee"; }; + var peg$f54 = function() { return "createdBy"; }; + var peg$f55 = function() { return "action"; }; + var peg$f56 = function() {return "total"; }; + var peg$f57 = function() {return "has"; }; + var peg$f58 = function() {return "is"; }; + var peg$f59 = function() {return "purchaseAmount"}; + var peg$f60 = function() {return "purchaseCurrency"}; + var peg$f61 = function() { isColumnsContext = true; return "columns"; }; - var peg$f61 = function() {return "view"}; - var peg$f62 = function() { return isColumnsContext; }; - var peg$f63 = function() { return "perDiem"; }; - var peg$f64 = function() { return "drafts"; }; - var peg$f65 = function() { return "originalamount"; }; - var peg$f66 = function() { return "taxAmount"; }; - var peg$f67 = function() { return "taxrate"; }; - var peg$f68 = function() { return "policyname"; }; - var peg$f69 = function() { return "withdrawalID"; }; - var peg$f70 = function() { return "bankAccount"; }; - var peg$f71 = function() { return "reportID"; }; - var peg$f72 = function() { return "base62ReportID"; }; - var peg$f73 = function() { return "exportedto"; }; - var peg$f74 = function() { return "exportedTo"; }; + var peg$f62 = function() {return "view"}; + var peg$f63 = function() { return isColumnsContext; }; + var peg$f64 = function() { return "perDiem"; }; + var peg$f65 = function() { return "drafts"; }; + var peg$f66 = function() { return "originalamount"; }; + var peg$f67 = function() { return "taxAmount"; }; + var peg$f68 = function() { return "taxrate"; }; + var peg$f69 = function() { return "policyname"; }; + var peg$f70 = function() { return "withdrawalID"; }; + var peg$f71 = function() { return "bankAccount"; }; + var peg$f72 = function() { return "reportID"; }; + var peg$f73 = function() { return "base62ReportID"; }; + var peg$f74 = function() { return "exportedto"; }; var peg$f75 = function() { return "exportedTo"; }; - var peg$f76 = function() { return "exchangeRate"; }; - var peg$f77 = function() { return "reimbursableTotal"; }; - var peg$f78 = function() { return "nonReimbursableTotal"; }; - var peg$f79 = function() { return "groupFrom"; }; - var peg$f80 = function() { return "groupExpenses"; }; - var peg$f81 = function() { return "groupTotal"; }; - var peg$f82 = function() { return "groupCard"; }; - var peg$f83 = function() { return "groupFeed"; }; - var peg$f84 = function() { return "groupBankAccount"; }; - var peg$f85 = function() { return "groupWithdrawn"; }; - var peg$f86 = function() { return "groupWithdrawalID"; }; - var peg$f87 = function() { return "groupAmountDebited"; }; - var peg$f88 = function() { return "groupAmountReimbursed"; }; - var peg$f89 = function() { return "groupCategory"; }; - var peg$f90 = function() { return "groupTag"; }; - var peg$f91 = function() { return "groupMerchant"; }; - var peg$f92 = function() { return "groupmonth"; }; - var peg$f93 = function() { return "groupweek"; }; - var peg$f94 = function() { return "groupyear"; }; - var peg$f95 = function() { return "groupquarter"; }; - var peg$f96 = function() { return "eq"; }; - var peg$f97 = function() { return "neq"; }; - var peg$f98 = function() { return "gte"; }; - var peg$f99 = function() { return "gt"; }; - var peg$f100 = function() { return "lte"; }; - var peg$f101 = function() { return "lt"; }; - var peg$f102 = function(o) { + var peg$f76 = function() { return "exportedTo"; }; + var peg$f77 = function() { return "exchangeRate"; }; + var peg$f78 = function() { return "reimbursableTotal"; }; + var peg$f79 = function() { return "nonReimbursableTotal"; }; + var peg$f80 = function() { return "groupFrom"; }; + var peg$f81 = function() { return "groupExpenses"; }; + var peg$f82 = function() { return "groupTotal"; }; + var peg$f83 = function() { return "groupCard"; }; + var peg$f84 = function() { return "groupFeed"; }; + var peg$f85 = function() { return "groupBankAccount"; }; + var peg$f86 = function() { return "groupWithdrawn"; }; + var peg$f87 = function() { return "groupWithdrawalID"; }; + var peg$f88 = function() { return "groupAmountDebited"; }; + var peg$f89 = function() { return "groupAmountReimbursed"; }; + var peg$f90 = function() { return "groupCategory"; }; + var peg$f91 = function() { return "groupTag"; }; + var peg$f92 = function() { return "groupMerchant"; }; + var peg$f93 = function() { return "groupmonth"; }; + var peg$f94 = function() { return "groupweek"; }; + var peg$f95 = function() { return "groupyear"; }; + var peg$f96 = function() { return "groupquarter"; }; + var peg$f97 = function() { return "eq"; }; + var peg$f98 = function() { return "neq"; }; + var peg$f99 = function() { return "gte"; }; + var peg$f100 = function() { return "gt"; }; + var peg$f101 = function() { return "lte"; }; + var peg$f102 = function() { return "lt"; }; + var peg$f103 = function(o) { if (nameOperator) { expectingNestedQuote = (o === "eq"); // Use simple parser if no valid operator is found } isColumnsContext = false; return o; }; - var peg$f103 = function(chars) { return chars.join("").trim(); }; - var peg$f104 = function() { + var peg$f104 = function(c) { return c; }; + var peg$f105 = function(chars) { return chars.join("").trim(); }; + var peg$f106 = function() { isColumnsContext = false; return "and"; }; - var peg$f105 = function() { return expectingNestedQuote; }; - var peg$f106 = function(start, inner, end) { //handle no-breaking space + var peg$f107 = function() { return expectingNestedQuote; }; + var peg$f108 = function(start, inner, end) { //handle no-breaking space return [...start, '"', ...inner, '"', ...end].join(""); }; - var peg$f107 = function(start) {return "“"}; - var peg$f108 = function(start) {return "”"}; - var peg$f109 = function(start) {return "\""}; - var peg$f110 = function(start, inner, end) { + var peg$f109 = function(start) {return "“"}; + var peg$f110 = function(start) {return "”"}; + var peg$f111 = function(start) {return "\""}; + var peg$f112 = function(start, inner, end) { return [...start, '"', ...inner, '"'].join(""); }; var peg$currPos = options.peg$currPos | 0; @@ -1231,16 +1237,30 @@ function peg$parse(input, options) { return s0; } + function peg$parseidentifierPart() { + var s0, s1; + + s0 = peg$currPos; + s1 = peg$parsequotedString(); + if (s1 === peg$FAILED) { + s1 = peg$parsealphanumeric(); + } + if (s1 !== peg$FAILED) { + peg$savedPos = s0; + s1 = peg$f6(s1); + } + s0 = s1; + + return s0; + } + function peg$parseidentifier() { var s0, s1, s2, s3, s4; s0 = peg$currPos; s1 = peg$currPos; s2 = []; - s3 = peg$parsequotedString(); - if (s3 === peg$FAILED) { - s3 = peg$parsealphanumeric(); - } + s3 = peg$parseidentifierPart(); while (s3 !== peg$FAILED) { s2.push(s3); s3 = peg$currPos; @@ -1252,10 +1272,7 @@ function peg$parse(input, options) { if (peg$silentFails === 0) { peg$fail(peg$e5); } } if (s4 !== peg$FAILED) { - s4 = peg$parsequotedString(); - if (s4 === peg$FAILED) { - s4 = peg$parsealphanumeric(); - } + s4 = peg$parseidentifierPart(); if (s4 === peg$FAILED) { peg$currPos = s3; s3 = peg$FAILED; @@ -1284,7 +1301,7 @@ function peg$parse(input, options) { s2 = null; } peg$savedPos = s0; - s0 = peg$f6(s1, s2); + s0 = peg$f7(s1, s2); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -1306,7 +1323,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f7(); + s1 = peg$f8(); } s0 = s1; @@ -1326,7 +1343,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f8(); + s1 = peg$f9(); } s0 = s1; @@ -1346,7 +1363,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f9(); + s1 = peg$f10(); } s0 = s1; @@ -1366,7 +1383,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f10(); + s1 = peg$f11(); } s0 = s1; @@ -1394,7 +1411,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f11(); + s1 = peg$f12(); } s0 = s1; } @@ -1415,7 +1432,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f12(); + s1 = peg$f13(); } s0 = s1; @@ -1435,7 +1452,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f13(); + s1 = peg$f14(); } s0 = s1; @@ -1455,7 +1472,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f14(); + s1 = peg$f15(); } s0 = s1; @@ -1483,7 +1500,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f15(); + s1 = peg$f16(); } s0 = s1; } @@ -1504,7 +1521,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f16(); + s1 = peg$f17(); } s0 = s1; @@ -1524,7 +1541,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f17(); + s1 = peg$f18(); } s0 = s1; @@ -1544,7 +1561,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f18(); + s1 = peg$f19(); } s0 = s1; @@ -1564,7 +1581,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f19(); + s1 = peg$f20(); } s0 = s1; @@ -1584,7 +1601,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f20(); + s1 = peg$f21(); } s0 = s1; @@ -1612,7 +1629,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f21(); + s1 = peg$f22(); } s0 = s1; } @@ -1641,7 +1658,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f22(); + s1 = peg$f23(); } s0 = s1; } @@ -1670,7 +1687,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f23(); + s1 = peg$f24(); } s0 = s1; } @@ -1691,7 +1708,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f24(); + s1 = peg$f25(); } s0 = s1; @@ -1711,7 +1728,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f25(); + s1 = peg$f26(); } s0 = s1; @@ -1739,7 +1756,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f26(); + s1 = peg$f27(); } s0 = s1; } @@ -1768,7 +1785,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f27(); + s1 = peg$f28(); } s0 = s1; } @@ -1797,7 +1814,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f28(); + s1 = peg$f29(); } s0 = s1; } @@ -1826,7 +1843,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f29(); + s1 = peg$f30(); } s0 = s1; } @@ -1855,7 +1872,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f30(); + s1 = peg$f31(); } s0 = s1; } @@ -1884,7 +1901,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f31(); + s1 = peg$f32(); } s0 = s1; } @@ -1905,7 +1922,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f32(); + s1 = peg$f33(); } s0 = s1; @@ -1925,7 +1942,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f33(); + s1 = peg$f34(); } s0 = s1; @@ -1945,7 +1962,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f34(); + s1 = peg$f35(); } s0 = s1; @@ -1965,7 +1982,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f35(); + s1 = peg$f36(); } s0 = s1; @@ -1993,7 +2010,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f36(); + s1 = peg$f37(); } s0 = s1; } @@ -2022,7 +2039,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f37(); + s1 = peg$f38(); } s0 = s1; } @@ -2051,7 +2068,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f38(); + s1 = peg$f39(); } s0 = s1; } @@ -2072,7 +2089,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f39(); + s1 = peg$f40(); } s0 = s1; @@ -2092,7 +2109,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f40(); + s1 = peg$f41(); } s0 = s1; @@ -2112,7 +2129,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f41(); + s1 = peg$f42(); } s0 = s1; @@ -2132,7 +2149,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f42(); + s1 = peg$f43(); } s0 = s1; @@ -2152,7 +2169,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f43(); + s1 = peg$f44(); } s0 = s1; @@ -2172,7 +2189,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f44(); + s1 = peg$f45(); } s0 = s1; @@ -2200,7 +2217,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f45(); + s1 = peg$f46(); } s0 = s1; } @@ -2221,7 +2238,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f46(); + s1 = peg$f47(); } s0 = s1; @@ -2241,7 +2258,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f47(); + s1 = peg$f48(); } s0 = s1; @@ -2261,7 +2278,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f48(); + s1 = peg$f49(); } s0 = s1; @@ -2289,7 +2306,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f49(); + s1 = peg$f50(); } s0 = s1; } @@ -2318,7 +2335,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f50(); + s1 = peg$f51(); } s0 = s1; } @@ -2347,7 +2364,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f51(); + s1 = peg$f52(); } s0 = s1; } @@ -2368,7 +2385,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f52(); + s1 = peg$f53(); } s0 = s1; @@ -2396,7 +2413,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f53(); + s1 = peg$f54(); } s0 = s1; } @@ -2417,7 +2434,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f54(); + s1 = peg$f55(); } s0 = s1; @@ -2437,7 +2454,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f55(); + s1 = peg$f56(); } s0 = s1; @@ -2457,7 +2474,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f56(); + s1 = peg$f57(); } s0 = s1; @@ -2477,7 +2494,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f57(); + s1 = peg$f58(); } s0 = s1; @@ -2505,7 +2522,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f58(); + s1 = peg$f59(); } s0 = s1; } @@ -2534,7 +2551,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f59(); + s1 = peg$f60(); } s0 = s1; } @@ -2555,7 +2572,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f60(); + s1 = peg$f61(); } s0 = s1; @@ -2575,7 +2592,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f61(); + s1 = peg$f62(); } s0 = s1; @@ -2587,7 +2604,7 @@ function peg$parse(input, options) { s0 = peg$currPos; peg$savedPos = peg$currPos; - s1 = peg$f62(); + s1 = peg$f63(); if (s1) { s1 = undefined; } else { @@ -2732,7 +2749,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f63(); + s0 = peg$f64(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2778,7 +2795,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f64(); + s0 = peg$f65(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2815,7 +2832,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f65(); + s0 = peg$f66(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2852,7 +2869,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f66(); + s0 = peg$f67(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2889,7 +2906,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f67(); + s0 = peg$f68(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2926,7 +2943,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f68(); + s0 = peg$f69(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -2963,7 +2980,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f69(); + s0 = peg$f70(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3000,7 +3017,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f70(); + s0 = peg$f71(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3037,7 +3054,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f71(); + s0 = peg$f72(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3074,7 +3091,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f72(); + s0 = peg$f73(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3111,7 +3128,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f73(); + s0 = peg$f74(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3174,7 +3191,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f74(); + s0 = peg$f75(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3205,7 +3222,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f75(); + s0 = peg$f76(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3243,7 +3260,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f76(); + s0 = peg$f77(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3280,7 +3297,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f77(); + s0 = peg$f78(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3317,7 +3334,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f78(); + s0 = peg$f79(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3354,7 +3371,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f79(); + s0 = peg$f80(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3391,7 +3408,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f80(); + s0 = peg$f81(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3428,7 +3445,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f81(); + s0 = peg$f82(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3465,7 +3482,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f82(); + s0 = peg$f83(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3502,7 +3519,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f83(); + s0 = peg$f84(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3539,7 +3556,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f84(); + s0 = peg$f85(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3576,7 +3593,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f85(); + s0 = peg$f86(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3613,7 +3630,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f86(); + s0 = peg$f87(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3650,7 +3667,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f87(); + s0 = peg$f88(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3687,7 +3704,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f88(); + s0 = peg$f89(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3724,7 +3741,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f89(); + s0 = peg$f90(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3761,7 +3778,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f90(); + s0 = peg$f91(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3798,7 +3815,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f91(); + s0 = peg$f92(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3835,7 +3852,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f92(); + s0 = peg$f93(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3872,7 +3889,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f93(); + s0 = peg$f94(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3909,7 +3926,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f94(); + s0 = peg$f95(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3946,7 +3963,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f95(); + s0 = peg$f96(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -3973,7 +3990,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f96(); + s1 = peg$f97(); } s0 = s1; if (s0 === peg$FAILED) { @@ -3987,7 +4004,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f97(); + s1 = peg$f98(); } s0 = s1; if (s0 === peg$FAILED) { @@ -4001,7 +4018,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f98(); + s1 = peg$f99(); } s0 = s1; if (s0 === peg$FAILED) { @@ -4015,7 +4032,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f99(); + s1 = peg$f100(); } s0 = s1; if (s0 === peg$FAILED) { @@ -4029,7 +4046,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f100(); + s1 = peg$f101(); } s0 = s1; if (s0 === peg$FAILED) { @@ -4043,7 +4060,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f101(); + s1 = peg$f102(); } s0 = s1; } @@ -4067,35 +4084,75 @@ function peg$parse(input, options) { s1 = peg$parseoperator(); if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f102(s1); + s1 = peg$f103(s1); } s0 = s1; return s0; } + function peg$parseescapedChar() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c113; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e118); } + } + if (s1 !== peg$FAILED) { + s2 = input.charAt(peg$currPos); + if (peg$r2.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e119); } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f104(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + function peg$parsealphanumeric() { var s0, s1, s2; peg$silentFails++; s0 = peg$currPos; s1 = []; - s2 = input.charAt(peg$currPos); - if (peg$r2.test(s2)) { - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e119); } + s2 = peg$parseescapedChar(); + if (s2 === peg$FAILED) { + s2 = input.charAt(peg$currPos); + if (peg$r3.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e121); } + } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { s1.push(s2); - s2 = input.charAt(peg$currPos); - if (peg$r2.test(s2)) { - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e119); } + s2 = peg$parseescapedChar(); + if (s2 === peg$FAILED) { + s2 = input.charAt(peg$currPos); + if (peg$r3.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e121); } + } } } } else { @@ -4103,13 +4160,13 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f103(s1); + s1 = peg$f105(s1); } s0 = s1; peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e118); } + if (peg$silentFails === 0) { peg$fail(peg$e120); } } return s0; @@ -4121,7 +4178,7 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = peg$parse_(); peg$savedPos = s0; - s1 = peg$f104(); + s1 = peg$f106(); s0 = s1; return s0; @@ -4133,25 +4190,25 @@ function peg$parse(input, options) { peg$silentFails++; s0 = []; s1 = input.charAt(peg$currPos); - if (peg$r3.test(s1)) { + if (peg$r4.test(s1)) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e121); } + if (peg$silentFails === 0) { peg$fail(peg$e123); } } while (s1 !== peg$FAILED) { s0.push(s1); s1 = input.charAt(peg$currPos); - if (peg$r3.test(s1)) { + if (peg$r4.test(s1)) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e121); } + if (peg$silentFails === 0) { peg$fail(peg$e123); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e120); } + if (peg$silentFails === 0) { peg$fail(peg$e122); } return s0; } @@ -4161,7 +4218,7 @@ function peg$parse(input, options) { s0 = peg$currPos; peg$savedPos = peg$currPos; - s1 = peg$f105(); + s1 = peg$f107(); if (s1) { s1 = undefined; } else { @@ -4194,76 +4251,82 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = []; s2 = input.charAt(peg$currPos); - if (peg$r4.test(s2)) { + if (peg$r5.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e123); } + if (peg$silentFails === 0) { peg$fail(peg$e125); } } while (s2 !== peg$FAILED) { s1.push(s2); s2 = input.charAt(peg$currPos); - if (peg$r4.test(s2)) { + if (peg$r5.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e123); } + if (peg$silentFails === 0) { peg$fail(peg$e125); } } } s2 = input.charAt(peg$currPos); - if (peg$r5.test(s2)) { + if (peg$r6.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e124); } + if (peg$silentFails === 0) { peg$fail(peg$e126); } } if (s2 !== peg$FAILED) { s3 = []; - s4 = input.charAt(peg$currPos); - if (peg$r6.test(s4)) { - peg$currPos++; - } else { - s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e125); } - } - while (s4 !== peg$FAILED) { - s3.push(s4); + s4 = peg$parseescapedChar(); + if (s4 === peg$FAILED) { s4 = input.charAt(peg$currPos); - if (peg$r6.test(s4)) { + if (peg$r7.test(s4)) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e125); } + if (peg$silentFails === 0) { peg$fail(peg$e127); } + } + } + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$parseescapedChar(); + if (s4 === peg$FAILED) { + s4 = input.charAt(peg$currPos); + if (peg$r7.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e127); } + } } } s4 = input.charAt(peg$currPos); - if (peg$r5.test(s4)) { + if (peg$r6.test(s4)) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e124); } + if (peg$silentFails === 0) { peg$fail(peg$e126); } } if (s4 !== peg$FAILED) { s5 = []; s6 = input.charAt(peg$currPos); - if (peg$r2.test(s6)) { + if (peg$r3.test(s6)) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e119); } + if (peg$silentFails === 0) { peg$fail(peg$e121); } } while (s6 !== peg$FAILED) { s5.push(s6); s6 = input.charAt(peg$currPos); - if (peg$r2.test(s6)) { + if (peg$r3.test(s6)) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e119); } + if (peg$silentFails === 0) { peg$fail(peg$e121); } } } peg$savedPos = s0; - s0 = peg$f106(s1, s3, s5); + s0 = peg$f108(s1, s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4275,7 +4338,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e122); } + if (peg$silentFails === 0) { peg$fail(peg$e124); } } return s0; @@ -4288,68 +4351,39 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = []; s2 = input.charAt(peg$currPos); - if (peg$r4.test(s2)) { + if (peg$r5.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e123); } + if (peg$silentFails === 0) { peg$fail(peg$e125); } } while (s2 !== peg$FAILED) { s1.push(s2); s2 = input.charAt(peg$currPos); - if (peg$r4.test(s2)) { + if (peg$r5.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e123); } + if (peg$silentFails === 0) { peg$fail(peg$e125); } } } s2 = input.charAt(peg$currPos); - if (peg$r5.test(s2)) { + if (peg$r6.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e124); } + if (peg$silentFails === 0) { peg$fail(peg$e126); } } if (s2 !== peg$FAILED) { s3 = []; - s4 = input.charAt(peg$currPos); - if (peg$r6.test(s4)) { - peg$currPos++; - } else { - s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e125); } - } + s4 = peg$parseescapedChar(); if (s4 === peg$FAILED) { - s4 = peg$currPos; - s5 = peg$currPos; - peg$silentFails++; - s6 = peg$parseclosingQuote(); - peg$silentFails--; - if (s6 === peg$FAILED) { - s5 = undefined; - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 8220) { - s6 = peg$c113; - peg$currPos++; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e126); } - } - if (s6 !== peg$FAILED) { - peg$savedPos = s4; - s4 = peg$f107(s1); - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } + s4 = input.charAt(peg$currPos); + if (peg$r7.test(s4)) { + peg$currPos++; } else { - peg$currPos = s4; s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e127); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -4364,16 +4398,16 @@ function peg$parse(input, options) { s5 = peg$FAILED; } if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 8221) { + if (input.charCodeAt(peg$currPos) === 8220) { s6 = peg$c114; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e127); } + if (peg$silentFails === 0) { peg$fail(peg$e128); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f108(s1); + s4 = peg$f109(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -4395,16 +4429,16 @@ function peg$parse(input, options) { s5 = peg$FAILED; } if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 34) { + if (input.charCodeAt(peg$currPos) === 8221) { s6 = peg$c115; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e128); } + if (peg$silentFails === 0) { peg$fail(peg$e129); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f109(s1); + s4 = peg$f110(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -4413,48 +4447,51 @@ function peg$parse(input, options) { peg$currPos = s4; s4 = peg$FAILED; } + if (s4 === peg$FAILED) { + s4 = peg$currPos; + s5 = peg$currPos; + peg$silentFails++; + s6 = peg$parseclosingQuote(); + peg$silentFails--; + if (s6 === peg$FAILED) { + s5 = undefined; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 34) { + s6 = peg$c116; + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e130); } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s4; + s4 = peg$f111(s1); + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } } } } while (s4 !== peg$FAILED) { s3.push(s4); - s4 = input.charAt(peg$currPos); - if (peg$r6.test(s4)) { - peg$currPos++; - } else { - s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e125); } - } + s4 = peg$parseescapedChar(); if (s4 === peg$FAILED) { - s4 = peg$currPos; - s5 = peg$currPos; - peg$silentFails++; - s6 = peg$parseclosingQuote(); - peg$silentFails--; - if (s6 === peg$FAILED) { - s5 = undefined; - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 8220) { - s6 = peg$c113; - peg$currPos++; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e126); } - } - if (s6 !== peg$FAILED) { - peg$savedPos = s4; - s4 = peg$f107(s1); - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } + s4 = input.charAt(peg$currPos); + if (peg$r7.test(s4)) { + peg$currPos++; } else { - peg$currPos = s4; s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e127); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -4469,16 +4506,16 @@ function peg$parse(input, options) { s5 = peg$FAILED; } if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 8221) { + if (input.charCodeAt(peg$currPos) === 8220) { s6 = peg$c114; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e127); } + if (peg$silentFails === 0) { peg$fail(peg$e128); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f108(s1); + s4 = peg$f109(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -4500,16 +4537,16 @@ function peg$parse(input, options) { s5 = peg$FAILED; } if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 34) { + if (input.charCodeAt(peg$currPos) === 8221) { s6 = peg$c115; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e128); } + if (peg$silentFails === 0) { peg$fail(peg$e129); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f109(s1); + s4 = peg$f110(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -4518,6 +4555,38 @@ function peg$parse(input, options) { peg$currPos = s4; s4 = peg$FAILED; } + if (s4 === peg$FAILED) { + s4 = peg$currPos; + s5 = peg$currPos; + peg$silentFails++; + s6 = peg$parseclosingQuote(); + peg$silentFails--; + if (s6 === peg$FAILED) { + s5 = undefined; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 34) { + s6 = peg$c116; + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e130); } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s4; + s4 = peg$f111(s1); + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } } } } @@ -4525,7 +4594,7 @@ function peg$parse(input, options) { s4 = peg$parseclosingQuote(); if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f110(s1, s3, s4); + s0 = peg$f112(s1, s3, s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4537,7 +4606,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e122); } + if (peg$silentFails === 0) { peg$fail(peg$e124); } } return s0; @@ -4548,11 +4617,11 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = input.charAt(peg$currPos); - if (peg$r5.test(s1)) { + if (peg$r6.test(s1)) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e124); } + if (peg$silentFails === 0) { peg$fail(peg$e126); } } if (s1 !== peg$FAILED) { s2 = peg$currPos; @@ -4586,56 +4655,56 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = []; s2 = input.charAt(peg$currPos); - if (peg$r7.test(s2)) { + if (peg$r8.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e129); } + if (peg$silentFails === 0) { peg$fail(peg$e131); } } while (s2 !== peg$FAILED) { s1.push(s2); s2 = input.charAt(peg$currPos); - if (peg$r7.test(s2)) { + if (peg$r8.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e129); } + if (peg$silentFails === 0) { peg$fail(peg$e131); } } } s2 = []; s3 = input.charAt(peg$currPos); - if (peg$r8.test(s3)) { + if (peg$r9.test(s3)) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e130); } + if (peg$silentFails === 0) { peg$fail(peg$e132); } } while (s3 !== peg$FAILED) { s2.push(s3); s3 = input.charAt(peg$currPos); - if (peg$r8.test(s3)) { + if (peg$r9.test(s3)) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e130); } + if (peg$silentFails === 0) { peg$fail(peg$e132); } } } s3 = []; s4 = input.charAt(peg$currPos); - if (peg$r9.test(s4)) { + if (peg$r10.test(s4)) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e131); } + if (peg$silentFails === 0) { peg$fail(peg$e133); } } while (s4 !== peg$FAILED) { s3.push(s4); s4 = input.charAt(peg$currPos); - if (peg$r9.test(s4)) { + if (peg$r10.test(s4)) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e131); } + if (peg$silentFails === 0) { peg$fail(peg$e133); } } } s4 = peg$parseoperator(); @@ -4650,20 +4719,20 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = []; s2 = input.charAt(peg$currPos); - if (peg$r10.test(s2)) { + if (peg$r11.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e132); } + if (peg$silentFails === 0) { peg$fail(peg$e134); } } while (s2 !== peg$FAILED) { s1.push(s2); s2 = input.charAt(peg$currPos); - if (peg$r10.test(s2)) { + if (peg$r11.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e132); } + if (peg$silentFails === 0) { peg$fail(peg$e134); } } } s2 = peg$currPos; @@ -4706,11 +4775,11 @@ function peg$parse(input, options) { } if (s0 === peg$FAILED) { s0 = input.charAt(peg$currPos); - if (peg$r11.test(s0)) { + if (peg$r12.test(s0)) { peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e133); } + if (peg$silentFails === 0) { peg$fail(peg$e135); } } } } @@ -4722,11 +4791,11 @@ function peg$parse(input, options) { var s0, s1; s0 = input.charAt(peg$currPos); - if (peg$r12.test(s0)) { + if (peg$r13.test(s0)) { peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e134); } + if (peg$silentFails === 0) { peg$fail(peg$e136); } } if (s0 === peg$FAILED) { s0 = peg$currPos; diff --git a/src/libs/SearchParser/autocompleteParser.peggy b/src/libs/SearchParser/autocompleteParser.peggy index 885f0cf30bfa..6349d88f9a73 100644 --- a/src/libs/SearchParser/autocompleteParser.peggy +++ b/src/libs/SearchParser/autocompleteParser.peggy @@ -137,26 +137,32 @@ filterKey return k; } +// Each part carries the offset and width of the text it actually matched. Walking the parsed values instead would +// under-count anything that was escaped, which shifts every range that follows it. +// The nested-quote branch of quotedString is a sequence, so it yields [undefined, value] rather than a bare string. +identifierPart + = part:(quotedString / alphanumeric) { + const value = Array.isArray(part) ? part.flat().filter(Boolean)[0] : part; + return {value, start: offset(), length: text().length}; + } + identifier - = parts:(quotedString / alphanumeric)|1.., ","| empty:","? { + = parts:identifierPart|1.., ","| empty:","? { const ends = location(); - const value = parts.flat().filter(Boolean); // Filter out undefined values returned by the predicate - if (empty) { - value.push(""); - } - let count = ends.start.offset; const result = []; - value.forEach((filter) => { - let word = filter; + parts.filter((part) => Boolean(part.value)).forEach((part) => { + let word = part.value; if (word.startsWith('"') && word.endsWith('"') && word.length >= 2) { word = word.slice(1, -1); } result.push({ value: word, - start: count, - length: filter.length, + start: part.start, + length: part.length, }); - count += filter.length + 1; }); + if (empty) { + result.push({value: "", start: ends.end.offset, length: 0}); + } return result; } diff --git a/src/libs/SearchParser/baseRules.peggy b/src/libs/SearchParser/baseRules.peggy index 40e4ce0b39dc..d1468a969cad 100644 --- a/src/libs/SearchParser/baseRules.peggy +++ b/src/libs/SearchParser/baseRules.peggy @@ -192,7 +192,11 @@ filterOperator return o; } -alphanumeric "word" = chars:[^ ,\t\n\r\xA0]+ { return chars.join("").trim(); } //handle no-breaking space +// A backslash only escapes the characters the grammar would otherwise consume. Anything else keeps the backslash +// as a literal, so values already persisted in saved searches and shared URLs still parse the way they used to. +escapedChar = "\\" c:[,"”“\\] { return c; } + +alphanumeric "word" = chars:(escapedChar / [^ ,\t\n\r\xA0])+ { return chars.join("").trim(); } //handle no-breaking space logicalAnd = _ { isColumnsContext = false; @@ -207,13 +211,14 @@ quotedString / simpleQuotedString simpleQuotedString "quote" - = start:[^ ,"”“\t\n\r\xA0]* ("“" / "\"" / "”") inner:[^"”“\r\n]* ("“" / "\"" / "”") end:[^ ,\t\n\r\xA0]* { //handle no-breaking space + = start:[^ ,"”“\t\n\r\xA0]* ("“" / "\"" / "”") inner:(escapedChar / [^"”“\r\n])* ("“" / "\"" / "”") end:[^ ,\t\n\r\xA0]* { //handle no-breaking space return [...start, '"', ...inner, '"', ...end].join(""); } nestedQuotedString "quote" = start:[^ ,"”“\t\n\r\xA0]* ("“" / "\"" / "”") // Opening quote - inner:([^"”“\r\n] + inner:(escapedChar + / [^"”“\r\n] / (!closingQuote "“" {return "“"}) / (!closingQuote "”" {return "”"}) / (!closingQuote "\"" {return "\""} ) )* // Allow anything inside, but exclude true closing quotes end:closingQuote // Detect the actual closing quote diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index 409c3b188df4..f6ec6b365ea9 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -297,24 +297,26 @@ function peg$parse(input, options) { var peg$c110 = ">"; var peg$c111 = "<="; var peg$c112 = "<"; - var peg$c113 = "\u201C"; - var peg$c114 = "\u201D"; - var peg$c115 = "\""; + var peg$c113 = "\\"; + var peg$c114 = "\u201C"; + var peg$c115 = "\u201D"; + var peg$c116 = "\""; var peg$r0 = /^[^ \t\r\n\xA0]/; var peg$r1 = /^[ \t\r\n\xA0,:=<>!]/; var peg$r2 = /^[:=]/; - var peg$r3 = /^[^ ,\t\n\r\xA0]/; - var peg$r4 = /^[ \t\r\n\xA0]/; - var peg$r5 = /^[^ ,"\u201D\u201C\t\n\r\xA0]/; - var peg$r6 = /^["\u201C-\u201D]/; - var peg$r7 = /^[^"\u201D\u201C\r\n]/; - var peg$r8 = /^[ \t\n\r\xA0a-zA-Z0-9]/; - var peg$r9 = /^[a-zA-Z0-9]/; - var peg$r10 = /^[ \t\n\r\xA0]/; - var peg$r11 = /^[ \t\n\r\xA0a-zA-Z]/; - var peg$r12 = /^[,]/; - var peg$r13 = /^[ \t\n\r\xA0,]/; + var peg$r3 = /^[,"\u201D\u201C\\]/; + var peg$r4 = /^[^ ,\t\n\r\xA0]/; + var peg$r5 = /^[ \t\r\n\xA0]/; + var peg$r6 = /^[^ ,"\u201D\u201C\t\n\r\xA0]/; + var peg$r7 = /^["\u201C-\u201D]/; + var peg$r8 = /^[^"\u201D\u201C\r\n]/; + var peg$r9 = /^[ \t\n\r\xA0a-zA-Z0-9]/; + var peg$r10 = /^[a-zA-Z0-9]/; + var peg$r11 = /^[ \t\n\r\xA0]/; + var peg$r12 = /^[ \t\n\r\xA0a-zA-Z]/; + var peg$r13 = /^[,]/; + var peg$r14 = /^[ \t\n\r\xA0,]/; var peg$e0 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], true, false); var peg$e1 = peg$literalExpectation("-", false); @@ -436,23 +438,25 @@ function peg$parse(input, options) { var peg$e117 = peg$literalExpectation(">", false); var peg$e118 = peg$literalExpectation("<=", false); var peg$e119 = peg$literalExpectation("<", false); - var peg$e120 = peg$otherExpectation("word"); - var peg$e121 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e122 = peg$otherExpectation("whitespace"); - var peg$e123 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); - var peg$e124 = peg$otherExpectation("quote"); - var peg$e125 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e126 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); - var peg$e127 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); - var peg$e128 = peg$literalExpectation("\u201C", false); - var peg$e129 = peg$literalExpectation("\u201D", false); - var peg$e130 = peg$literalExpectation("\"", false); - var peg$e131 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e132 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e133 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); - var peg$e134 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); - var peg$e135 = peg$classExpectation([","], false, false); - var peg$e136 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ","], false, false); + var peg$e120 = peg$literalExpectation("\\", false); + var peg$e121 = peg$classExpectation([",", "\"", "\u201D", "\u201C", "\\"], false, false); + var peg$e122 = peg$otherExpectation("word"); + var peg$e123 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e124 = peg$otherExpectation("whitespace"); + var peg$e125 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); + var peg$e126 = peg$otherExpectation("quote"); + var peg$e127 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e128 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); + var peg$e129 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); + var peg$e130 = peg$literalExpectation("\u201C", false); + var peg$e131 = peg$literalExpectation("\u201D", false); + var peg$e132 = peg$literalExpectation("\"", false); + var peg$e133 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e134 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e135 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); + var peg$e136 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); + var peg$e137 = peg$classExpectation([","], false, false); + var peg$e138 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ","], false, false); var peg$f0 = function(filters) { return applyDefaults(filters); }; var peg$f1 = function(head, tail) { @@ -644,19 +648,20 @@ function peg$parse(input, options) { isColumnsContext = false; return o; }; - var peg$f104 = function(chars) { return chars.join("").trim(); }; - var peg$f105 = function() { + var peg$f104 = function(c) { return c; }; + var peg$f105 = function(chars) { return chars.join("").trim(); }; + var peg$f106 = function() { isColumnsContext = false; return "and"; }; - var peg$f106 = function() { return expectingNestedQuote; }; - var peg$f107 = function(start, inner, end) { //handle no-breaking space + var peg$f107 = function() { return expectingNestedQuote; }; + var peg$f108 = function(start, inner, end) { //handle no-breaking space return [...start, '"', ...inner, '"', ...end].join(""); }; - var peg$f108 = function(start) {return "“"}; - var peg$f109 = function(start) {return "”"}; - var peg$f110 = function(start) {return "\""}; - var peg$f111 = function(start, inner, end) { + var peg$f109 = function(start) {return "“"}; + var peg$f110 = function(start) {return "”"}; + var peg$f111 = function(start) {return "\""}; + var peg$f112 = function(start, inner, end) { return [...start, '"', ...inner, '"'].join(""); }; var peg$currPos = options.peg$currPos | 0; @@ -4258,28 +4263,68 @@ function peg$parse(input, options) { return s0; } + function peg$parseescapedChar() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c113; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e120); } + } + if (s1 !== peg$FAILED) { + s2 = input.charAt(peg$currPos); + if (peg$r3.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e121); } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f104(s2); + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + } else { + peg$currPos = s0; + s0 = peg$FAILED; + } + + return s0; + } + function peg$parsealphanumeric() { var s0, s1, s2; peg$silentFails++; s0 = peg$currPos; s1 = []; - s2 = input.charAt(peg$currPos); - if (peg$r3.test(s2)) { - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e121); } + s2 = peg$parseescapedChar(); + if (s2 === peg$FAILED) { + s2 = input.charAt(peg$currPos); + if (peg$r4.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e123); } + } } if (s2 !== peg$FAILED) { while (s2 !== peg$FAILED) { s1.push(s2); - s2 = input.charAt(peg$currPos); - if (peg$r3.test(s2)) { - peg$currPos++; - } else { - s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e121); } + s2 = peg$parseescapedChar(); + if (s2 === peg$FAILED) { + s2 = input.charAt(peg$currPos); + if (peg$r4.test(s2)) { + peg$currPos++; + } else { + s2 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e123); } + } } } } else { @@ -4287,13 +4332,13 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f104(s1); + s1 = peg$f105(s1); } s0 = s1; peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e120); } + if (peg$silentFails === 0) { peg$fail(peg$e122); } } return s0; @@ -4305,7 +4350,7 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = peg$parse_(); peg$savedPos = s0; - s1 = peg$f105(); + s1 = peg$f106(); s0 = s1; return s0; @@ -4317,25 +4362,25 @@ function peg$parse(input, options) { peg$silentFails++; s0 = []; s1 = input.charAt(peg$currPos); - if (peg$r4.test(s1)) { + if (peg$r5.test(s1)) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e123); } + if (peg$silentFails === 0) { peg$fail(peg$e125); } } while (s1 !== peg$FAILED) { s0.push(s1); s1 = input.charAt(peg$currPos); - if (peg$r4.test(s1)) { + if (peg$r5.test(s1)) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e123); } + if (peg$silentFails === 0) { peg$fail(peg$e125); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e122); } + if (peg$silentFails === 0) { peg$fail(peg$e124); } return s0; } @@ -4345,7 +4390,7 @@ function peg$parse(input, options) { s0 = peg$currPos; peg$savedPos = peg$currPos; - s1 = peg$f106(); + s1 = peg$f107(); if (s1) { s1 = undefined; } else { @@ -4378,76 +4423,82 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = []; s2 = input.charAt(peg$currPos); - if (peg$r5.test(s2)) { + if (peg$r6.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e125); } + if (peg$silentFails === 0) { peg$fail(peg$e127); } } while (s2 !== peg$FAILED) { s1.push(s2); s2 = input.charAt(peg$currPos); - if (peg$r5.test(s2)) { + if (peg$r6.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e125); } + if (peg$silentFails === 0) { peg$fail(peg$e127); } } } s2 = input.charAt(peg$currPos); - if (peg$r6.test(s2)) { + if (peg$r7.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e126); } + if (peg$silentFails === 0) { peg$fail(peg$e128); } } if (s2 !== peg$FAILED) { s3 = []; - s4 = input.charAt(peg$currPos); - if (peg$r7.test(s4)) { - peg$currPos++; - } else { - s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e127); } - } - while (s4 !== peg$FAILED) { - s3.push(s4); + s4 = peg$parseescapedChar(); + if (s4 === peg$FAILED) { s4 = input.charAt(peg$currPos); - if (peg$r7.test(s4)) { + if (peg$r8.test(s4)) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e127); } + if (peg$silentFails === 0) { peg$fail(peg$e129); } + } + } + while (s4 !== peg$FAILED) { + s3.push(s4); + s4 = peg$parseescapedChar(); + if (s4 === peg$FAILED) { + s4 = input.charAt(peg$currPos); + if (peg$r8.test(s4)) { + peg$currPos++; + } else { + s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e129); } + } } } s4 = input.charAt(peg$currPos); - if (peg$r6.test(s4)) { + if (peg$r7.test(s4)) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e126); } + if (peg$silentFails === 0) { peg$fail(peg$e128); } } if (s4 !== peg$FAILED) { s5 = []; s6 = input.charAt(peg$currPos); - if (peg$r3.test(s6)) { + if (peg$r4.test(s6)) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e121); } + if (peg$silentFails === 0) { peg$fail(peg$e123); } } while (s6 !== peg$FAILED) { s5.push(s6); s6 = input.charAt(peg$currPos); - if (peg$r3.test(s6)) { + if (peg$r4.test(s6)) { peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e121); } + if (peg$silentFails === 0) { peg$fail(peg$e123); } } } peg$savedPos = s0; - s0 = peg$f107(s1, s3, s5); + s0 = peg$f108(s1, s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4459,7 +4510,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e124); } + if (peg$silentFails === 0) { peg$fail(peg$e126); } } return s0; @@ -4472,68 +4523,39 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = []; s2 = input.charAt(peg$currPos); - if (peg$r5.test(s2)) { + if (peg$r6.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e125); } + if (peg$silentFails === 0) { peg$fail(peg$e127); } } while (s2 !== peg$FAILED) { s1.push(s2); s2 = input.charAt(peg$currPos); - if (peg$r5.test(s2)) { + if (peg$r6.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e125); } + if (peg$silentFails === 0) { peg$fail(peg$e127); } } } s2 = input.charAt(peg$currPos); - if (peg$r6.test(s2)) { + if (peg$r7.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e126); } + if (peg$silentFails === 0) { peg$fail(peg$e128); } } if (s2 !== peg$FAILED) { s3 = []; - s4 = input.charAt(peg$currPos); - if (peg$r7.test(s4)) { - peg$currPos++; - } else { - s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e127); } - } + s4 = peg$parseescapedChar(); if (s4 === peg$FAILED) { - s4 = peg$currPos; - s5 = peg$currPos; - peg$silentFails++; - s6 = peg$parseclosingQuote(); - peg$silentFails--; - if (s6 === peg$FAILED) { - s5 = undefined; - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 8220) { - s6 = peg$c113; - peg$currPos++; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e128); } - } - if (s6 !== peg$FAILED) { - peg$savedPos = s4; - s4 = peg$f108(s1); - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } + s4 = input.charAt(peg$currPos); + if (peg$r8.test(s4)) { + peg$currPos++; } else { - peg$currPos = s4; s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e129); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -4548,12 +4570,12 @@ function peg$parse(input, options) { s5 = peg$FAILED; } if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 8221) { + if (input.charCodeAt(peg$currPos) === 8220) { s6 = peg$c114; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e129); } + if (peg$silentFails === 0) { peg$fail(peg$e130); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; @@ -4579,12 +4601,12 @@ function peg$parse(input, options) { s5 = peg$FAILED; } if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 34) { + if (input.charCodeAt(peg$currPos) === 8221) { s6 = peg$c115; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e130); } + if (peg$silentFails === 0) { peg$fail(peg$e131); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; @@ -4597,48 +4619,51 @@ function peg$parse(input, options) { peg$currPos = s4; s4 = peg$FAILED; } + if (s4 === peg$FAILED) { + s4 = peg$currPos; + s5 = peg$currPos; + peg$silentFails++; + s6 = peg$parseclosingQuote(); + peg$silentFails--; + if (s6 === peg$FAILED) { + s5 = undefined; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 34) { + s6 = peg$c116; + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e132); } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s4; + s4 = peg$f111(s1); + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } } } } while (s4 !== peg$FAILED) { s3.push(s4); - s4 = input.charAt(peg$currPos); - if (peg$r7.test(s4)) { - peg$currPos++; - } else { - s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e127); } - } + s4 = peg$parseescapedChar(); if (s4 === peg$FAILED) { - s4 = peg$currPos; - s5 = peg$currPos; - peg$silentFails++; - s6 = peg$parseclosingQuote(); - peg$silentFails--; - if (s6 === peg$FAILED) { - s5 = undefined; - } else { - peg$currPos = s5; - s5 = peg$FAILED; - } - if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 8220) { - s6 = peg$c113; - peg$currPos++; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e128); } - } - if (s6 !== peg$FAILED) { - peg$savedPos = s4; - s4 = peg$f108(s1); - } else { - peg$currPos = s4; - s4 = peg$FAILED; - } + s4 = input.charAt(peg$currPos); + if (peg$r8.test(s4)) { + peg$currPos++; } else { - peg$currPos = s4; s4 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e129); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -4653,12 +4678,12 @@ function peg$parse(input, options) { s5 = peg$FAILED; } if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 8221) { + if (input.charCodeAt(peg$currPos) === 8220) { s6 = peg$c114; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e129); } + if (peg$silentFails === 0) { peg$fail(peg$e130); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; @@ -4684,12 +4709,12 @@ function peg$parse(input, options) { s5 = peg$FAILED; } if (s5 !== peg$FAILED) { - if (input.charCodeAt(peg$currPos) === 34) { + if (input.charCodeAt(peg$currPos) === 8221) { s6 = peg$c115; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e130); } + if (peg$silentFails === 0) { peg$fail(peg$e131); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; @@ -4702,6 +4727,38 @@ function peg$parse(input, options) { peg$currPos = s4; s4 = peg$FAILED; } + if (s4 === peg$FAILED) { + s4 = peg$currPos; + s5 = peg$currPos; + peg$silentFails++; + s6 = peg$parseclosingQuote(); + peg$silentFails--; + if (s6 === peg$FAILED) { + s5 = undefined; + } else { + peg$currPos = s5; + s5 = peg$FAILED; + } + if (s5 !== peg$FAILED) { + if (input.charCodeAt(peg$currPos) === 34) { + s6 = peg$c116; + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e132); } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s4; + s4 = peg$f111(s1); + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } } } } @@ -4709,7 +4766,7 @@ function peg$parse(input, options) { s4 = peg$parseclosingQuote(); if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f111(s1, s3, s4); + s0 = peg$f112(s1, s3, s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4721,7 +4778,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e124); } + if (peg$silentFails === 0) { peg$fail(peg$e126); } } return s0; @@ -4732,11 +4789,11 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = input.charAt(peg$currPos); - if (peg$r6.test(s1)) { + if (peg$r7.test(s1)) { peg$currPos++; } else { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e126); } + if (peg$silentFails === 0) { peg$fail(peg$e128); } } if (s1 !== peg$FAILED) { s2 = peg$currPos; @@ -4770,56 +4827,56 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = []; s2 = input.charAt(peg$currPos); - if (peg$r8.test(s2)) { + if (peg$r9.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e131); } + if (peg$silentFails === 0) { peg$fail(peg$e133); } } while (s2 !== peg$FAILED) { s1.push(s2); s2 = input.charAt(peg$currPos); - if (peg$r8.test(s2)) { + if (peg$r9.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e131); } + if (peg$silentFails === 0) { peg$fail(peg$e133); } } } s2 = []; s3 = input.charAt(peg$currPos); - if (peg$r9.test(s3)) { + if (peg$r10.test(s3)) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e132); } + if (peg$silentFails === 0) { peg$fail(peg$e134); } } while (s3 !== peg$FAILED) { s2.push(s3); s3 = input.charAt(peg$currPos); - if (peg$r9.test(s3)) { + if (peg$r10.test(s3)) { peg$currPos++; } else { s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e132); } + if (peg$silentFails === 0) { peg$fail(peg$e134); } } } s3 = []; s4 = input.charAt(peg$currPos); - if (peg$r10.test(s4)) { + if (peg$r11.test(s4)) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e133); } + if (peg$silentFails === 0) { peg$fail(peg$e135); } } while (s4 !== peg$FAILED) { s3.push(s4); s4 = input.charAt(peg$currPos); - if (peg$r10.test(s4)) { + if (peg$r11.test(s4)) { peg$currPos++; } else { s4 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e133); } + if (peg$silentFails === 0) { peg$fail(peg$e135); } } } s4 = peg$parseoperator(); @@ -4834,20 +4891,20 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = []; s2 = input.charAt(peg$currPos); - if (peg$r11.test(s2)) { + if (peg$r12.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e134); } + if (peg$silentFails === 0) { peg$fail(peg$e136); } } while (s2 !== peg$FAILED) { s1.push(s2); s2 = input.charAt(peg$currPos); - if (peg$r11.test(s2)) { + if (peg$r12.test(s2)) { peg$currPos++; } else { s2 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e134); } + if (peg$silentFails === 0) { peg$fail(peg$e136); } } } s2 = peg$currPos; @@ -4890,11 +4947,11 @@ function peg$parse(input, options) { } if (s0 === peg$FAILED) { s0 = input.charAt(peg$currPos); - if (peg$r12.test(s0)) { + if (peg$r13.test(s0)) { peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e135); } + if (peg$silentFails === 0) { peg$fail(peg$e137); } } } } @@ -4906,11 +4963,11 @@ function peg$parse(input, options) { var s0, s1; s0 = input.charAt(peg$currPos); - if (peg$r13.test(s0)) { + if (peg$r14.test(s0)) { peg$currPos++; } else { s0 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e136); } + if (peg$silentFails === 0) { peg$fail(peg$e138); } } if (s0 === peg$FAILED) { s0 = peg$currPos; diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 933eae6b2473..11b98028f4f5 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -165,17 +165,16 @@ function getUserFriendlyValue(value: string | undefined): UserFriendlyValue { /** * @private - * Returns string value wrapped in quotes "", if the value contains space,   (no-breaking space), or a comma when shouldQuoteComma is set. + * Escapes the characters the parser would otherwise act on, then wraps the value in quotes "" if it contains a space, + *   (no-breaking space), or a comma when shouldQuoteComma is set. A comma inside quotes needs no escape, so a + * value that held one before still serializes the same way and keeps its query hash. */ function sanitizeSearchValue(str: string, shouldQuoteComma = false) { - if (str.includes(' ') || str.includes(`\xA0`) || (shouldQuoteComma && str.includes(','))) { - return `"${str}"`; + const escaped = str.replaceAll(/[\\"“”]/g, '\\$&'); + if (escaped.includes(' ') || escaped.includes(`\xA0`) || (shouldQuoteComma && escaped.includes(','))) { + return `"${escaped}"`; } - return str; -} - -function stripSearchValueQuotes(str: string) { - return str.replaceAll(/["“”]/g, ''); + return escaped; } const syntaxRegex = new RegExp(`^-?(${Object.values(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS).join('|')}|report-?field(-.+)+)[:><=].+$`); @@ -458,11 +457,33 @@ function getFilterFromQuery(queryJSON: SearchQueryJSON | undefined, filterKey: S return {value, isNegated}; } +let allPolicies: OnyxCollection = {}; +Onyx.connectWithoutView({ + key: ONYXKEYS.COLLECTION.POLICY, + callback: (policies) => { + allPolicies = policies ?? {}; + }, +}); + +/** + * Resolves a typed workspace name to its ID. Names are not unique, so an ambiguous one is left alone rather than + * guessing which workspace was meant. + */ +function resolvePolicyIDFromName(value: string, policies: OnyxCollection) { + if (policies?.[`${ONYXKEYS.COLLECTION.POLICY}${value}`]) { + return value; + } + + const matches = Object.values(policies ?? {}).filter((policy) => policy?.name?.toLowerCase() === value.toLowerCase()); + return matches.length === 1 ? (matches.at(0)?.id ?? value) : value; +} + /** * @private * Returns an updated filter value for some query filters. * - for `AMOUNT` it formats value to "backend" amount * - for personal filters it tries to substitute any user emails with accountIDs + * - for `POLICY_ID` it tries to substitute an unambiguous workspace name with its ID */ function getUpdatedFilterValue(filterName: SyntaxFilterKey, filterValue: string | string[], shouldSkipAmountConversion = false) { if (AMOUNT_FILTER_KEYS.includes(filterName as SearchAmountFilterKeys)) { @@ -493,6 +514,13 @@ function getUpdatedFilterValue(filterName: SyntaxFilterKey, filterValue: string return filterValue.map((email) => getPersonalDetailByEmail(email)?.accountID.toString() ?? email); } + if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID) { + if (typeof filterValue === 'string') { + return resolvePolicyIDFromName(filterValue, allPolicies); + } + return filterValue.map((value) => resolvePolicyIDFromName(value, allPolicies)); + } + if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_ID || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_ID) { const cleanIDs = (value: string) => value @@ -2611,8 +2639,8 @@ export { buildQueryStringFromFilterFormValues, buildFilterFormValuesFromQuery, buildCannedSearchQuery, + resolvePolicyIDFromName, sanitizeSearchValue, - stripSearchValueQuotes, getQueryWithUpdatedValues, getKeywordQueryWithCurrentSearchContext, getCurrentSearchQueryJSON, diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 88e86db37be8..7b2d5eb747ed 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -37,12 +37,12 @@ import { isSearchBeforeViolationsSnapshotStarted, isSearchRootParams, serializeQueryJSONForBackend, + resolvePolicyIDFromName, sanitizeSearchValue, shouldHighlight, shouldResetSort, shouldResetSortForViewChange, sortOptionsWithEmptyValue, - stripSearchValueQuotes, withExactMatchFilterKeys, } from '@src/libs/SearchQueryUtils'; import NAVIGATORS from '@src/NAVIGATORS'; @@ -4115,16 +4115,52 @@ describe('SearchQueryUtils', () => { expect(sanitizeSearchValue('Acme,Inc')).toBe('Acme,Inc'); expect(sanitizeSearchValue('Acme,Inc', true)).toBe('"Acme,Inc"'); }); + + it('escapes quotes and backslashes so the parser reads them as part of the value', () => { + expect(sanitizeSearchValue('A"B')).toBe('A\\"B'); + expect(sanitizeSearchValue('A\\B')).toBe('A\\\\B'); + expect(sanitizeSearchValue('Acme "US",Inc', true)).toBe('"Acme \\"US\\",Inc"'); + expect(sanitizeSearchValue('Acme “US” Inc')).toBe('"Acme \\“US\\” Inc"'); + }); + + it('serializes a value without a quote or backslash exactly as before, so its query hash is unchanged', () => { + expect(sanitizeSearchValue('Acme, Inc.')).toBe('"Acme, Inc."'); + expect(sanitizeSearchValue('Acme,Inc', true)).toBe('"Acme,Inc"'); + expect(sanitizeSearchValue('Travel')).toBe('Travel'); + }); }); - describe('stripSearchValueQuotes', () => { - it('removes straight and curly quotes', () => { - expect(stripSearchValueQuotes('Acme,"Inc')).toBe('Acme,Inc'); - expect(stripSearchValueQuotes('Acme “US” Inc')).toBe('Acme US Inc'); + describe('resolvePolicyIDFromName', () => { + const ACME_ID = '26BE5C4005E188DB'; + const OTHER_ID = '312ECD05D0CD4B27'; + const policies = { + [`${ONYXKEYS.COLLECTION.POLICY}${ACME_ID}`]: {...createRandomPolicy(1, undefined, 'Acme, Inc.'), id: ACME_ID}, + [`${ONYXKEYS.COLLECTION.POLICY}${OTHER_ID}`]: {...createRandomPolicy(2, undefined, 'Beta Corp'), id: OTHER_ID}, + }; + + it('resolves a name that matches exactly one workspace', () => { + expect(resolvePolicyIDFromName('Acme, Inc.', policies)).toBe(ACME_ID); }); - it('leaves a value without quotes untouched', () => { - expect(stripSearchValueQuotes('Acme, Inc.')).toBe('Acme, Inc.'); + it('matches the name regardless of case', () => { + expect(resolvePolicyIDFromName('acme, inc.', policies)).toBe(ACME_ID); + }); + + it('leaves a value that is already a policy ID alone', () => { + expect(resolvePolicyIDFromName(ACME_ID, policies)).toBe(ACME_ID); + }); + + it('leaves an unknown name alone', () => { + expect(resolvePolicyIDFromName('Nonexistent', policies)).toBe('Nonexistent'); + }); + + it('leaves an ambiguous name alone rather than guessing', () => { + const duplicates = { + ...policies, + [`${ONYXKEYS.COLLECTION.POLICY}${OTHER_ID}`]: {...createRandomPolicy(2, undefined, 'Acme, Inc.'), id: OTHER_ID}, + }; + + expect(resolvePolicyIDFromName('Acme, Inc.', duplicates)).toBe('Acme, Inc.'); }); }); }); diff --git a/tests/unit/SearchAutocompleteParserTest.ts b/tests/unit/SearchAutocompleteParserTest.ts index 4e7184965662..1d2fdaeb3e3f 100644 --- a/tests/unit/SearchAutocompleteParserTest.ts +++ b/tests/unit/SearchAutocompleteParserTest.ts @@ -790,3 +790,35 @@ describe('autocomplete parser - limit filter', () => { expect(result).toEqual(expected); }); }); + +describe('autocomplete parser - escaped values', () => { + test.each([ + ['workspace:"Acme \\"US\\",Inc"', 'policyID', 'Acme "US",Inc'], + ['in:"Acme \\"US\\",Inc"', 'in', 'Acme "US",Inc'], + ['from:"Bob \\"The Builder\\" Smith"', 'from', 'Bob "The Builder" Smith'], + ['workspace:Acme\\,Inc', 'policyID', 'Acme,Inc'], + ])('reads %s back as a single value', (query, key, value) => { + const {ranges} = parse(query) as {ranges: Array<{key: string; value: string}>}; + + expect(ranges.filter((range) => range.key === key).map((range) => range.value)).toEqual([value]); + }); + + test.each([ + ['workspace:"Acme, Inc."', 'policyID', 'Acme, Inc.'], + ['workspace:"Acme,Inc"', 'policyID', 'Acme,Inc'], + ['workspace:A\\B', 'policyID', 'A\\B'], + ['merchant:"C:\\Users"', 'merchant', 'C:\\Users'], + ])('leaves an already persisted value %s unchanged', (query, key, value) => { + const {ranges} = parse(query) as {ranges: Array<{key: string; value: string}>}; + + expect(ranges.filter((range) => range.key === key).map((range) => range.value)).toEqual([value]); + }); + + it('reports a range that spans the escaped source text, so substitutions splice cleanly', () => { + const query = 'workspace:"Acme \\"US\\",Inc"'; + const {ranges} = parse(query) as {ranges: Array<{key: string; start: number; length: number}>}; + const range = ranges.find((candidate) => candidate.key === 'policyID'); + + expect(query.slice(range?.start, (range?.start ?? 0) + (range?.length ?? 0))).toBe('"Acme \\"US\\",Inc"'); + }); +}); diff --git a/tests/unit/SearchAutocompleteUtilsTest.ts b/tests/unit/SearchAutocompleteUtilsTest.ts index 6e4012ed488a..759d48acc635 100644 --- a/tests/unit/SearchAutocompleteUtilsTest.ts +++ b/tests/unit/SearchAutocompleteUtilsTest.ts @@ -1,7 +1,7 @@ import type {SubstitutionMap} from '@components/Search/SearchRouter/getQueryWithSubstitutions'; import {getSearchValueForConnection, getStandardExportTemplateDisplayName, isStandardExportTemplate, isStandardExportTemplateLabel} from '@libs/AccountingUtils'; -import {getParsableSearchValue, getTrimmedUserSearchQueryPreservingComma, parseForLiveMarkdown} from '@libs/SearchAutocompleteUtils'; +import {getTrimmedUserSearchQueryPreservingComma, parseForLiveMarkdown} from '@libs/SearchAutocompleteUtils'; import CONST from '@src/CONST'; @@ -588,34 +588,4 @@ describe('SearchAutocompleteUtils', () => { }); }); }); - - describe('getParsableSearchValue', () => { - describe('name filters, which the parser reads back with quotes intact', () => { - it.each(['from', 'to', 'payer', 'assignee'])('keeps a quoted display name for %s', (filterKey) => { - expect(getParsableSearchValue(filterKey, 'Bob "The Builder" Smith')).toBe('Bob "The Builder" Smith'); - }); - - it('keeps a plain display name', () => { - expect(getParsableSearchValue('from', 'Alice Smith')).toBe('Alice Smith'); - }); - - it('strips quotes only when the name cannot be read back as one value', () => { - expect(getParsableSearchValue('from', 'Acme "US",Inc')).toBe('Acme US,Inc'); - }); - }); - - describe('workspace and room filters, which cannot carry quotes', () => { - it('strips quotes from a workspace name', () => { - expect(getParsableSearchValue('workspace', 'Bob "The Builder" Smith')).toBe('Bob The Builder Smith'); - }); - - it('keeps a workspace name containing only a comma', () => { - expect(getParsableSearchValue('workspace', 'Acme,Inc')).toBe('Acme,Inc'); - }); - - it('keeps a room name whose quote survives the round trip', () => { - expect(getParsableSearchValue('in', 'Acme,"Inc')).toBe('Acme,"Inc'); - }); - }); - }); }); diff --git a/tests/unit/SearchRouterUtilsTest.ts b/tests/unit/SearchRouterUtilsTest.ts index 4ef2299e2f7b..63a3d7e1ced7 100644 --- a/tests/unit/SearchRouterUtilsTest.ts +++ b/tests/unit/SearchRouterUtilsTest.ts @@ -213,7 +213,7 @@ describe('SearchRouterUtils', () => { expect(submittedQuery).toBe(`type:expense workspace:${POLICY_ID}`); }); - it('resolves a workspace name containing both a quote and a comma to its policy ID', () => { + it('keeps a workspace name containing both a quote and a comma, and resolves it to its policy ID', () => { const item: SearchQueryItem = { keyForList: POLICY_ID, roomType: CONST.SEARCH.DATA_TYPES.EXPENSE, @@ -224,11 +224,11 @@ describe('SearchRouterUtils', () => { const {seededQuery, submittedQuery} = submitContextualSuggestion(item, buildPolicies('Acme "US",Inc')); - expect(seededQuery).toBe('type:expense workspace:"Acme US,Inc"'); + expect(seededQuery).toBe('type:expense workspace:"Acme \\"US\\",Inc"'); expect(submittedQuery).toBe(`type:expense workspace:${POLICY_ID}`); }); - it('resolves a workspace name containing a quote and a space to its policy ID', () => { + it('keeps a workspace name containing a quote and a space, and resolves it to its policy ID', () => { const item: SearchQueryItem = { keyForList: POLICY_ID, roomType: CONST.SEARCH.DATA_TYPES.EXPENSE, @@ -237,8 +237,9 @@ describe('SearchRouterUtils', () => { searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.CONTEXTUAL_SUGGESTION, }; - const {submittedQuery} = submitContextualSuggestion(item, buildPolicies('Acme "US" Inc')); + const {seededQuery, submittedQuery} = submitContextualSuggestion(item, buildPolicies('Acme "US" Inc')); + expect(seededQuery).toBe('type:expense workspace:"Acme \\"US\\" Inc"'); expect(submittedQuery).toBe(`type:expense workspace:${POLICY_ID}`); }); From 479170d3fab4f0f767792aa77baee0b0e18e83ec Mon Sep 17 00:00:00 2001 From: Mukher Date: Thu, 13 Aug 2026 08:44:49 +0500 Subject: [PATCH 2/4] Quote every value that contains a comma when rebuilding a query Submitting a query rebuilds it through buildFilterValuesString, which asked sanitizeSearchValue not to quote commas. A value that holds one was written back bare, so `merchant:Acme\,Inc` and `merchant:"Acme,Inc"` both came out as `merchant:Acme,Inc` and the next read took them as two merchants. The quoted form was already lost this way before the escape character existed. Callers pass one value at a time and join them with commas themselves, so a comma inside a value is always part of it. Quote on it unconditionally and drop the opt in, which only invited a caller to forget it. A list of values is unaffected, since each value is sanitized on its own and only one that contains a comma gains quotes. Take the display name lookup out of the loop that compares it, justify the policy subscription, and read ranges back through the typed autocomplete wrapper so the tests add no assertions. --- .../Search/SearchRouter/SearchRouter.tsx | 2 +- .../Search/SearchRouter/SearchRouterUtils.ts | 9 +++----- src/libs/SearchQueryUtils.ts | 13 +++++++---- tests/unit/Search/SearchQueryUtilsTest.ts | 23 ++++++++++++------- tests/unit/SearchAutocompleteParserTest.ts | 7 +++--- 5 files changed, 31 insertions(+), 23 deletions(-) diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 2b39ce2bcc6a..785a05905650 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -419,7 +419,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla } else if (item.searchItemType === CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.AUTOCOMPLETE_SUGGESTION && textInputValue) { const fieldKey = item.mapKey?.includes(':') ? item.mapKey.split(':').at(0) : item.mapKey; const trimmedUserSearchQuery = getTrimmedUserSearchQueryPreservingComma(textInputValue, fieldKey); - const newSearchQuery = `${trimmedUserSearchQuery}${sanitizeSearchValue(item.searchQuery, true)}\u00A0`; + const newSearchQuery = `${trimmedUserSearchQuery}${sanitizeSearchValue(item.searchQuery)}\u00A0`; onSearchQueryChange(newSearchQuery, true); setSelection({start: newSearchQuery.length, end: newSearchQuery.length}); diff --git a/src/components/Search/SearchRouter/SearchRouterUtils.ts b/src/components/Search/SearchRouter/SearchRouterUtils.ts index 91e903166381..dc69694b7804 100644 --- a/src/components/Search/SearchRouter/SearchRouterUtils.ts +++ b/src/components/Search/SearchRouter/SearchRouterUtils.ts @@ -74,20 +74,17 @@ function getContextualSearchQuery(item: SearchQueryItem, policies: OnyxCollectio switch (item.roomType) { case CONST.SEARCH.DATA_TYPES.EXPENSE: - additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID}:${sanitizeSearchValue( - item.policyID ? getPolicyNameWithFallback(item.policyID, policies, reports) : '', - true, - )}`; + additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID}:${sanitizeSearchValue(item.policyID ? getPolicyNameWithFallback(item.policyID, policies, reports) : '')}`; break; case CONST.SEARCH.DATA_TYPES.INVOICE: additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID}:${item.policyID}`; if (item.autocompleteID) { - additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TO}:${sanitizeSearchValue(item.searchQuery ?? '', true)}`; + additionalQuery += ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TO}:${sanitizeSearchValue(item.searchQuery ?? '')}`; } break; case CONST.SEARCH.DATA_TYPES.CHAT: default: - additionalQuery = ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.IN}:${sanitizeSearchValue(item.searchQuery ?? '', true)}`; + additionalQuery = ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.IN}:${sanitizeSearchValue(item.searchQuery ?? '')}`; break; } return baseQuery + additionalQuery; diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index 11b98028f4f5..f77930db74fd 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -166,12 +166,12 @@ function getUserFriendlyValue(value: string | undefined): UserFriendlyValue { /** * @private * Escapes the characters the parser would otherwise act on, then wraps the value in quotes "" if it contains a space, - *   (no-breaking space), or a comma when shouldQuoteComma is set. A comma inside quotes needs no escape, so a - * value that held one before still serializes the same way and keeps its query hash. + *   (no-breaking space) or a comma. Callers pass one value at a time and join them with commas themselves, so a + * comma inside the value is part of it and has to survive being read back. */ -function sanitizeSearchValue(str: string, shouldQuoteComma = false) { +function sanitizeSearchValue(str: string) { const escaped = str.replaceAll(/[\\"“”]/g, '\\$&'); - if (escaped.includes(' ') || escaped.includes(`\xA0`) || (shouldQuoteComma && escaped.includes(','))) { + if (escaped.includes(' ') || escaped.includes(`\xA0`) || escaped.includes(',')) { return `"${escaped}"`; } return escaped; @@ -457,6 +457,8 @@ function getFilterFromQuery(queryJSON: SearchQueryJSON | undefined, filterKey: S return {value, isNegated}; } +// getUpdatedFilterValue runs while building a query, outside React, and already resolves emails through the cache in +// PersonalDetailsUtils. Threading policies through it would mean a parameter on every caller of getQueryWithUpdatedValues. let allPolicies: OnyxCollection = {}; Onyx.connectWithoutView({ key: ONYXKEYS.COLLECTION.POLICY, @@ -474,7 +476,8 @@ function resolvePolicyIDFromName(value: string, policies: OnyxCollection policy?.name?.toLowerCase() === value.toLowerCase()); + const lowerCaseValue = value.toLowerCase(); + const matches = Object.values(policies ?? {}).filter((policy) => policy?.name?.toLowerCase() === lowerCaseValue); return matches.length === 1 ? (matches.at(0)?.id ?? value) : value; } diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 7b2d5eb747ed..9689eee72112 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -317,6 +317,15 @@ describe('SearchQueryUtils', () => { expect(result).toEqual(`${defaultQuery} amount:2000000 foo test`); }); + test('rebuilds a single value containing a comma as one value', () => { + expect(getQueryWithUpdatedValues(String.raw`merchant:Acme\,Inc`)).toEqual(`${defaultQuery} merchant:"Acme,Inc"`); + expect(getQueryWithUpdatedValues('merchant:"Acme,Inc"')).toEqual(`${defaultQuery} merchant:"Acme,Inc"`); + }); + + test('rebuilds a comma separated list as separate values', () => { + expect(getQueryWithUpdatedValues('category:Travel,Meals')).toEqual(`${defaultQuery} category:Travel,Meals`); + }); + test('returns query with user emails substituted', () => { const userQuery = 'from:johndoe@example.com hello'; @@ -464,12 +473,12 @@ describe('SearchQueryUtils', () => { test('has empty category values', () => { const filterValues: Partial = { type: 'expense', - category: ['equipment', 'consulting', 'none,Uncategorized'], + category: ['equipment', 'consulting', CONST.SEARCH.CATEGORY_EMPTY_VALUE], }; const result = buildQueryStringFromFilterFormValues(filterValues); - expect(result).toEqual('type:expense category:equipment,consulting,none,Uncategorized'); + expect(result).toEqual(`type:expense category:equipment,consulting,${CONST.SEARCH.CATEGORY_EMPTY_VALUE}`); }); test('empty filter values', () => { @@ -4111,21 +4120,19 @@ describe('SearchQueryUtils', () => { expect(sanitizeSearchValue('Acme\xA0Inc')).toBe('"Acme\xA0Inc"'); }); - it('only quotes on a comma when asked to', () => { - expect(sanitizeSearchValue('Acme,Inc')).toBe('Acme,Inc'); - expect(sanitizeSearchValue('Acme,Inc', true)).toBe('"Acme,Inc"'); + it('quotes on a comma, so a value containing one is not read back as two', () => { + expect(sanitizeSearchValue('Acme,Inc')).toBe('"Acme,Inc"'); }); it('escapes quotes and backslashes so the parser reads them as part of the value', () => { expect(sanitizeSearchValue('A"B')).toBe('A\\"B'); expect(sanitizeSearchValue('A\\B')).toBe('A\\\\B'); - expect(sanitizeSearchValue('Acme "US",Inc', true)).toBe('"Acme \\"US\\",Inc"'); + expect(sanitizeSearchValue('Acme "US",Inc')).toBe('"Acme \\"US\\",Inc"'); expect(sanitizeSearchValue('Acme “US” Inc')).toBe('"Acme \\“US\\” Inc"'); }); - it('serializes a value without a quote or backslash exactly as before, so its query hash is unchanged', () => { + it('serializes a value with no character needing escaping exactly as before', () => { expect(sanitizeSearchValue('Acme, Inc.')).toBe('"Acme, Inc."'); - expect(sanitizeSearchValue('Acme,Inc', true)).toBe('"Acme,Inc"'); expect(sanitizeSearchValue('Travel')).toBe('Travel'); }); }); diff --git a/tests/unit/SearchAutocompleteParserTest.ts b/tests/unit/SearchAutocompleteParserTest.ts index 1d2fdaeb3e3f..80a42bb67645 100644 --- a/tests/unit/SearchAutocompleteParserTest.ts +++ b/tests/unit/SearchAutocompleteParserTest.ts @@ -1,5 +1,6 @@ import type {SearchQueryJSON} from '@components/Search/types'; +import {parseForAutocomplete} from '@libs/SearchAutocompleteUtils'; import {parse} from '@libs/SearchParser/autocompleteParser'; import parserCommonTests from '../utils/fixtures/searchParsersCommonQueries'; @@ -798,7 +799,7 @@ describe('autocomplete parser - escaped values', () => { ['from:"Bob \\"The Builder\\" Smith"', 'from', 'Bob "The Builder" Smith'], ['workspace:Acme\\,Inc', 'policyID', 'Acme,Inc'], ])('reads %s back as a single value', (query, key, value) => { - const {ranges} = parse(query) as {ranges: Array<{key: string; value: string}>}; + const ranges = parseForAutocomplete(query)?.ranges ?? []; expect(ranges.filter((range) => range.key === key).map((range) => range.value)).toEqual([value]); }); @@ -809,14 +810,14 @@ describe('autocomplete parser - escaped values', () => { ['workspace:A\\B', 'policyID', 'A\\B'], ['merchant:"C:\\Users"', 'merchant', 'C:\\Users'], ])('leaves an already persisted value %s unchanged', (query, key, value) => { - const {ranges} = parse(query) as {ranges: Array<{key: string; value: string}>}; + const ranges = parseForAutocomplete(query)?.ranges ?? []; expect(ranges.filter((range) => range.key === key).map((range) => range.value)).toEqual([value]); }); it('reports a range that spans the escaped source text, so substitutions splice cleanly', () => { const query = 'workspace:"Acme \\"US\\",Inc"'; - const {ranges} = parse(query) as {ranges: Array<{key: string; start: number; length: number}>}; + const ranges = parseForAutocomplete(query)?.ranges ?? []; const range = ranges.find((candidate) => candidate.key === 'policyID'); expect(query.slice(range?.start, (range?.start ?? 0) + (range?.length ?? 0))).toBe('"Acme \\"US\\",Inc"'); From 1c7af07f26f9b0bffc85305afd001ebdae372f90 Mon Sep 17 00:00:00 2001 From: Mukher Date: Thu, 13 Aug 2026 13:33:04 +0500 Subject: [PATCH 3/4] Pass policies in rather than holding a second copy Resolving a typed workspace name read a module level cache filled by its own Onyx subscription, a second copy of data the callers already hold and one whose timing nothing guarantees. Take the collection as a parameter instead. Every caller is a component or a hook that already reads it, so the value now comes from a subscription that has settled before the render that uses it. Drop the workspace named with a bare comma from the tests. The remaining cases still cover a comma inside a value, which is what the quoting is for, under a name that reads as one workspace rather than two. --- .../SearchPageHeader/SearchPageInput.tsx | 5 +++- .../Search/SearchRouter/SearchRouter.tsx | 4 ++-- .../Search/hooks/useUpdateFilterQuery.tsx | 2 ++ src/libs/SearchQueryUtils.ts | 23 ++++++------------- tests/unit/Search/SearchQueryUtilsTest.ts | 6 ++--- tests/unit/SearchAutocompleteParserTest.ts | 4 ++-- tests/unit/SearchRouterUtilsTest.ts | 15 ------------ 7 files changed, 20 insertions(+), 39 deletions(-) diff --git a/src/components/Search/SearchPageHeader/SearchPageInput.tsx b/src/components/Search/SearchPageHeader/SearchPageInput.tsx index 89dbf3dbdcf6..021bade62604 100644 --- a/src/components/Search/SearchPageHeader/SearchPageInput.tsx +++ b/src/components/Search/SearchPageHeader/SearchPageInput.tsx @@ -2,6 +2,7 @@ import type {SearchQueryJSON} from '@components/Search/types'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -12,6 +13,7 @@ import {getKeywordQueryWithCurrentSearchContext, getQueryWithUpdatedValues, sani import variables from '@styles/variables'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import KeyboardUtils from '@src/utils/keyboard'; @@ -27,6 +29,7 @@ type SearchPageInputProps = { function SearchPageInput({queryJSON, onFocus}: SearchPageInputProps) { const {translate} = useLocalize(); + const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); const styles = useThemeStyles(); const theme = useTheme(); const {shouldUseNarrowLayout} = useResponsiveLayout(); @@ -44,7 +47,7 @@ function SearchPageInput({queryJSON, onFocus}: SearchPageInputProps) { function submitSearch(query: string) { const queryWithContext = getKeywordQueryWithCurrentSearchContext(query, queryJSON); - const updatedQuery = getQueryWithUpdatedValues(queryWithContext); + const updatedQuery = getQueryWithUpdatedValues(queryWithContext, false, policies); if (!updatedQuery) { return; diff --git a/src/components/Search/SearchRouter/SearchRouter.tsx b/src/components/Search/SearchRouter/SearchRouter.tsx index 785a05905650..696dcca918d9 100644 --- a/src/components/Search/SearchRouter/SearchRouter.tsx +++ b/src/components/Search/SearchRouter/SearchRouter.tsx @@ -360,7 +360,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla const submitSearch = useCallback( (queryString: SearchQueryString, shouldSkipAmountConversion = false) => { const queryWithSubstitutions = getQueryWithSubstitutions(queryString, autocompleteSubstitutions, currentUserAccountID); - const updatedQuery = getQueryWithUpdatedValues(queryWithSubstitutions, shouldSkipAmountConversion); + const updatedQuery = getQueryWithUpdatedValues(queryWithSubstitutions, shouldSkipAmountConversion, policies); if (!updatedQuery) { return; } @@ -379,7 +379,7 @@ function SearchRouter({onRouterClose, shouldHideInputCaret, isSearchRouterDispla setTextInputValue(''); setAutocompleteQueryValue(''); }, - [autocompleteSubstitutions, currentUserAccountID, onRouterClose, setTextInputValue, setShouldResetSearchQuery, isFromSearchPageSearchButton], + [autocompleteSubstitutions, currentUserAccountID, onRouterClose, setTextInputValue, setShouldResetSearchQuery, isFromSearchPageSearchButton, policies], ); const onListItemPress = useCallback( diff --git a/src/components/Search/hooks/useUpdateFilterQuery.tsx b/src/components/Search/hooks/useUpdateFilterQuery.tsx index 4dff2aefe8c2..1a1e171dc90d 100644 --- a/src/components/Search/hooks/useUpdateFilterQuery.tsx +++ b/src/components/Search/hooks/useUpdateFilterQuery.tsx @@ -14,6 +14,7 @@ import {getEmptyObject} from '@src/types/utils/EmptyObject'; function useUpdateFilterQuery(queryJSON: SearchQueryJSON | undefined) { const {translate} = useLocalize(); const [searchAdvancedFiltersForm = getEmptyObject>()] = useOnyx(ONYXKEYS.FORMS.SEARCH_ADVANCED_FILTERS_FORM); + const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY); function getUpdatedFilterFormValues(currentValues: Partial, newValues: Partial) { const updatedFilterFormValues: Partial = { @@ -42,6 +43,7 @@ function useUpdateFilterQuery(queryJSON: SearchQueryJSON | undefined) { values, {view: searchAdvancedFiltersForm.view, groupBy: searchAdvancedFiltersForm.groupBy}, {sortBy: queryJSON?.sortBy, sortOrder: queryJSON?.sortOrder}, + policies, ) ?? ''; if (!queryString) { return; diff --git a/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index f77930db74fd..b7ff3757e19a 100644 --- a/src/libs/SearchQueryUtils.ts +++ b/src/libs/SearchQueryUtils.ts @@ -457,16 +457,6 @@ function getFilterFromQuery(queryJSON: SearchQueryJSON | undefined, filterKey: S return {value, isNegated}; } -// getUpdatedFilterValue runs while building a query, outside React, and already resolves emails through the cache in -// PersonalDetailsUtils. Threading policies through it would mean a parameter on every caller of getQueryWithUpdatedValues. -let allPolicies: OnyxCollection = {}; -Onyx.connectWithoutView({ - key: ONYXKEYS.COLLECTION.POLICY, - callback: (policies) => { - allPolicies = policies ?? {}; - }, -}); - /** * Resolves a typed workspace name to its ID. Names are not unique, so an ambiguous one is left alone rather than * guessing which workspace was meant. @@ -488,7 +478,7 @@ function resolvePolicyIDFromName(value: string, policies: OnyxCollection) { if (AMOUNT_FILTER_KEYS.includes(filterName as SearchAmountFilterKeys)) { if (shouldSkipAmountConversion) { return filterValue; @@ -519,9 +509,9 @@ function getUpdatedFilterValue(filterName: SyntaxFilterKey, filterValue: string if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.POLICY_ID) { if (typeof filterValue === 'string') { - return resolvePolicyIDFromName(filterValue, allPolicies); + return resolvePolicyIDFromName(filterValue, policies); } - return filterValue.map((value) => resolvePolicyIDFromName(value, allPolicies)); + return filterValue.map((value) => resolvePolicyIDFromName(value, policies)); } if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_ID || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_ID) { @@ -2208,7 +2198,7 @@ function getKeywordQueryWithCurrentSearchContext(queryString: SearchQueryString, * Returns new string query, after parsing it and traversing to update some filter values. * If there are any personal emails, it will try to substitute them with accountIDs */ -function getQueryWithUpdatedValues(query: string, shouldSkipAmountConversion = false) { +function getQueryWithUpdatedValues(query: string, shouldSkipAmountConversion = false, policies?: OnyxCollection) { const queryJSON = buildSearchQueryJSON(query); if (!queryJSON) { @@ -2216,7 +2206,7 @@ function getQueryWithUpdatedValues(query: string, shouldSkipAmountConversion = f return; } - const computeNodeValue = (left: SyntaxFilterKey, right: string | string[]) => getUpdatedFilterValue(left, right, shouldSkipAmountConversion); + const computeNodeValue = (left: SyntaxFilterKey, right: string | string[]) => getUpdatedFilterValue(left, right, shouldSkipAmountConversion, policies); const standardizedQuery = traverseAndUpdatedQuery(queryJSON, computeNodeValue); const rawFilterList = getRawFilterListFromQuery(query); const hasInFilter = rawFilterList?.some((filter) => !filter.isDefault && filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.IN) ?? false; @@ -2416,6 +2406,7 @@ function buildFilterQueryWithSortDefaults( filterValues: Partial, previousState: {view?: string; groupBy?: string}, currentQueryOptions: {sortBy?: string; sortOrder?: string}, + policies?: OnyxCollection, ): string | undefined { const resetSort = shouldResetSort({ newGroupBy: filterValues.groupBy, @@ -2439,7 +2430,7 @@ function buildFilterQueryWithSortDefaults( return queryString; } - return getQueryWithUpdatedValues(queryString, true); + return getQueryWithUpdatedValues(queryString, true, policies); } /** diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 9689eee72112..4bb05fd709bc 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -318,8 +318,8 @@ describe('SearchQueryUtils', () => { }); test('rebuilds a single value containing a comma as one value', () => { - expect(getQueryWithUpdatedValues(String.raw`merchant:Acme\,Inc`)).toEqual(`${defaultQuery} merchant:"Acme,Inc"`); - expect(getQueryWithUpdatedValues('merchant:"Acme,Inc"')).toEqual(`${defaultQuery} merchant:"Acme,Inc"`); + expect(getQueryWithUpdatedValues(String.raw`merchant:Globex\,Ltd`)).toEqual(`${defaultQuery} merchant:"Globex,Ltd"`); + expect(getQueryWithUpdatedValues('merchant:"Globex,Ltd"')).toEqual(`${defaultQuery} merchant:"Globex,Ltd"`); }); test('rebuilds a comma separated list as separate values', () => { @@ -4121,7 +4121,7 @@ describe('SearchQueryUtils', () => { }); it('quotes on a comma, so a value containing one is not read back as two', () => { - expect(sanitizeSearchValue('Acme,Inc')).toBe('"Acme,Inc"'); + expect(sanitizeSearchValue('Globex,Ltd')).toBe('"Globex,Ltd"'); }); it('escapes quotes and backslashes so the parser reads them as part of the value', () => { diff --git a/tests/unit/SearchAutocompleteParserTest.ts b/tests/unit/SearchAutocompleteParserTest.ts index 80a42bb67645..8214022e3c11 100644 --- a/tests/unit/SearchAutocompleteParserTest.ts +++ b/tests/unit/SearchAutocompleteParserTest.ts @@ -797,7 +797,7 @@ describe('autocomplete parser - escaped values', () => { ['workspace:"Acme \\"US\\",Inc"', 'policyID', 'Acme "US",Inc'], ['in:"Acme \\"US\\",Inc"', 'in', 'Acme "US",Inc'], ['from:"Bob \\"The Builder\\" Smith"', 'from', 'Bob "The Builder" Smith'], - ['workspace:Acme\\,Inc', 'policyID', 'Acme,Inc'], + ['workspace:Globex\\,Ltd', 'policyID', 'Globex,Ltd'], ])('reads %s back as a single value', (query, key, value) => { const ranges = parseForAutocomplete(query)?.ranges ?? []; @@ -806,7 +806,7 @@ describe('autocomplete parser - escaped values', () => { test.each([ ['workspace:"Acme, Inc."', 'policyID', 'Acme, Inc.'], - ['workspace:"Acme,Inc"', 'policyID', 'Acme,Inc'], + ['workspace:"Globex,Ltd"', 'policyID', 'Globex,Ltd'], ['workspace:A\\B', 'policyID', 'A\\B'], ['merchant:"C:\\Users"', 'merchant', 'C:\\Users'], ])('leaves an already persisted value %s unchanged', (query, key, value) => { diff --git a/tests/unit/SearchRouterUtilsTest.ts b/tests/unit/SearchRouterUtilsTest.ts index 63a3d7e1ced7..4cf6bc5723ae 100644 --- a/tests/unit/SearchRouterUtilsTest.ts +++ b/tests/unit/SearchRouterUtilsTest.ts @@ -198,21 +198,6 @@ describe('SearchRouterUtils', () => { return {seededQuery, submittedQuery: getQueryWithSubstitutions(seededQuery, substitutions)}; } - it('resolves a workspace name containing a comma to its policy ID', () => { - const item: SearchQueryItem = { - keyForList: POLICY_ID, - roomType: CONST.SEARCH.DATA_TYPES.EXPENSE, - policyID: POLICY_ID, - autocompleteID: POLICY_ID, - searchItemType: CONST.SEARCH.SEARCH_ROUTER_ITEM_TYPE.CONTEXTUAL_SUGGESTION, - }; - - const {seededQuery, submittedQuery} = submitContextualSuggestion(item, buildPolicies('Acme,Inc')); - - expect(seededQuery).toBe('type:expense workspace:"Acme,Inc"'); - expect(submittedQuery).toBe(`type:expense workspace:${POLICY_ID}`); - }); - it('keeps a workspace name containing both a quote and a comma, and resolves it to its policy ID', () => { const item: SearchQueryItem = { keyForList: POLICY_ID, From e265ac8304219d9fc8c54e218d9cabc90d603301 Mon Sep 17 00:00:00 2001 From: Mukher Date: Thu, 13 Aug 2026 14:21:23 +0500 Subject: [PATCH 4/4] Teach the free-text branch the same escape syntax as filter values --- src/libs/SearchParser/searchParser.js | 30 ++++++++++++++--------- src/libs/SearchParser/searchParser.peggy | 2 +- tests/unit/Search/SearchQueryUtilsTest.ts | 11 +++++++++ 3 files changed, 30 insertions(+), 13 deletions(-) diff --git a/src/libs/SearchParser/searchParser.js b/src/libs/SearchParser/searchParser.js index f6ec6b365ea9..5f4736124a96 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -942,22 +942,28 @@ function peg$parse(input, options) { s2 = peg$parsequotedString(); if (s2 === peg$FAILED) { s2 = []; - s3 = input.charAt(peg$currPos); - if (peg$r0.test(s3)) { - peg$currPos++; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e0); } + s3 = peg$parseescapedChar(); + if (s3 === peg$FAILED) { + s3 = input.charAt(peg$currPos); + if (peg$r0.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } } if (s3 !== peg$FAILED) { while (s3 !== peg$FAILED) { s2.push(s3); - s3 = input.charAt(peg$currPos); - if (peg$r0.test(s3)) { - peg$currPos++; - } else { - s3 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e0); } + s3 = peg$parseescapedChar(); + if (s3 === peg$FAILED) { + s3 = input.charAt(peg$currPos); + if (peg$r0.test(s3)) { + peg$currPos++; + } else { + s3 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e0); } + } } } } else { diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy index 7a48b0da4e3a..6f6f58e26f1f 100644 --- a/src/libs/SearchParser/searchParser.peggy +++ b/src/libs/SearchParser/searchParser.peggy @@ -219,7 +219,7 @@ defaultFilter } freeTextFilter - = _ value:(quotedString / [^ \t\r\n\xA0]+) _ { + = _ value:(quotedString / (escapedChar / [^ \t\r\n\xA0])+) _ { //handle no-breaking space let word; if (Array.isArray(value)) { diff --git a/tests/unit/Search/SearchQueryUtilsTest.ts b/tests/unit/Search/SearchQueryUtilsTest.ts index 4bb05fd709bc..df2cca930066 100644 --- a/tests/unit/Search/SearchQueryUtilsTest.ts +++ b/tests/unit/Search/SearchQueryUtilsTest.ts @@ -2381,6 +2381,17 @@ describe('SearchQueryUtils', () => { expect(getFilterFromQuery(newQueryJSON, CONST.SEARCH.SYNTAX_FILTER_KEYS.STATUS).value).toBe(undefined); }); + test.each([ + ['a straight quote', 'A"B'], + ['a curly quote', 'A“B'], + ['a backslash', 'A\\B'], + ])('round-trips a bare keyword containing %s', (_label, keyword) => { + const result = buildSearchQueryString(buildSearchQueryJSON(`type:expense ${keyword}`)); + + const keywordFilter = buildSearchQueryJSON(result)?.flatFilters.find((filter) => filter.key === CONST.SEARCH.SYNTAX_FILTER_KEYS.KEYWORD); + expect(keywordFilter?.filters.at(0)?.value).toBe(keyword); + }); + test('does not add quotes to non-keyword filter values', () => { const queryJSON = buildSearchQueryJSON('type:expense merchant:Amazon');