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/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 2b39ce2bcc6a..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( @@ -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 ad608cdfd7fb..dc69694b7804 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) : ''}`; } } @@ -78,26 +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( - getParsableSearchValue(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.POLICY_ID, 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( - getParsableSearchValue(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.TO, 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( - getParsableSearchValue(CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.IN, item.searchQuery ?? ''), - true, - )}`; + additionalQuery = ` ${CONST.SEARCH.SEARCH_USER_FRIENDLY_KEYS.IN}:${sanitizeSearchValue(item.searchQuery ?? '')}`; break; } return baseQuery + additionalQuery; 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/SearchAutocompleteUtils.ts b/src/libs/SearchAutocompleteUtils.ts index 9ed3d3a94af3..dde15bc131df 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. */ @@ -351,7 +337,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 d68f3c39a7a6..79d9a7f88d5e 100644 --- a/src/libs/SearchParser/autocompleteParser.js +++ b/src/libs/SearchParser/autocompleteParser.js @@ -300,23 +300,25 @@ function peg$parse(input, options) { var peg$c114 = ">"; var peg$c115 = "<="; var peg$c116 = "<"; - var peg$c117 = "\u201C"; - var peg$c118 = "\u201D"; - var peg$c119 = "\""; + var peg$c117 = "\\"; + var peg$c118 = "\u201C"; + var peg$c119 = "\u201D"; + var peg$c120 = "\""; 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); @@ -440,23 +442,25 @@ function peg$parse(input, options) { var peg$e119 = peg$literalExpectation(">", false); var peg$e120 = peg$literalExpectation("<=", false); var peg$e121 = peg$literalExpectation("<", 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$e122 = peg$literalExpectation("\\", false); + var peg$e123 = peg$classExpectation([",", "\"", "\u201D", "\u201C", "\\"], false, false); + var peg$e124 = peg$otherExpectation("word"); + var peg$e125 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e126 = peg$otherExpectation("whitespace"); + var peg$e127 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); + var peg$e128 = peg$otherExpectation("quote"); + var peg$e129 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e130 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); + var peg$e131 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); + var peg$e132 = peg$literalExpectation("\u201C", false); + var peg$e133 = peg$literalExpectation("\u201D", false); + var peg$e134 = peg$literalExpectation("\"", false); + var peg$e135 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e136 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e137 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); + var peg$e138 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); + var peg$e139 = peg$classExpectation([","], false, false); + var peg$e140 = 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(); }; @@ -498,150 +502,152 @@ 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() {return "amountDebited"}; - var peg$f61 = function() {return "amountReimbursed"}; - var peg$f62 = 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() {return "amountDebited"}; + var peg$f62 = function() {return "amountReimbursed"}; + var peg$f63 = function() { isColumnsContext = true; return "columns"; }; - var peg$f63 = function() {return "view"}; - var peg$f64 = function() { return isColumnsContext; }; - var peg$f65 = function() { return "perDiem"; }; - var peg$f66 = function() { return "drafts"; }; - var peg$f67 = function() { return "originalamount"; }; - var peg$f68 = function() { return "taxAmount"; }; - var peg$f69 = function() { return "taxrate"; }; - var peg$f70 = function() { return "policyname"; }; - var peg$f71 = function() { return "withdrawalID"; }; - var peg$f72 = function() { return "bankAccount"; }; - var peg$f73 = function() { return "reportID"; }; - var peg$f74 = function() { return "base62ReportID"; }; - var peg$f75 = function() { return "exportedto"; }; - var peg$f76 = function() { return "exportedTo"; }; + var peg$f64 = function() {return "view"}; + var peg$f65 = function() { return isColumnsContext; }; + var peg$f66 = function() { return "perDiem"; }; + var peg$f67 = function() { return "drafts"; }; + var peg$f68 = function() { return "originalamount"; }; + var peg$f69 = function() { return "taxAmount"; }; + var peg$f70 = function() { return "taxrate"; }; + var peg$f71 = function() { return "policyname"; }; + var peg$f72 = function() { return "withdrawalID"; }; + var peg$f73 = function() { return "bankAccount"; }; + var peg$f74 = function() { return "reportID"; }; + var peg$f75 = function() { return "base62ReportID"; }; + var peg$f76 = function() { return "exportedto"; }; var peg$f77 = function() { return "exportedTo"; }; - var peg$f78 = function() { return "exchangeRate"; }; - var peg$f79 = function() { return "reimbursableTotal"; }; - var peg$f80 = function() { return "nonReimbursableTotal"; }; - var peg$f81 = function() { return "groupFrom"; }; - var peg$f82 = function() { return "groupExpenses"; }; - var peg$f83 = function() { return "groupTotal"; }; - var peg$f84 = function() { return "groupCard"; }; - var peg$f85 = function() { return "groupFeed"; }; - var peg$f86 = function() { return "groupBankAccount"; }; - var peg$f87 = function() { return "groupWithdrawn"; }; - var peg$f88 = function() { return "groupWithdrawalID"; }; - var peg$f89 = function() { return "groupAmountDebited"; }; - var peg$f90 = function() { return "groupAmountReimbursed"; }; - var peg$f91 = function() { return "amountDebited"; }; - var peg$f92 = function() { return "amountReimbursed"; }; - var peg$f93 = function() { return "groupCategory"; }; - var peg$f94 = function() { return "groupTag"; }; - var peg$f95 = function() { return "groupMerchant"; }; - var peg$f96 = function() { return "groupmonth"; }; - var peg$f97 = function() { return "groupweek"; }; - var peg$f98 = function() { return "groupyear"; }; - var peg$f99 = function() { return "groupquarter"; }; - var peg$f100 = function() { return "eq"; }; - var peg$f101 = function() { return "neq"; }; - var peg$f102 = function() { return "gte"; }; - var peg$f103 = function() { return "gt"; }; - var peg$f104 = function() { return "lte"; }; - var peg$f105 = function() { return "lt"; }; - var peg$f106 = function(o) { + var peg$f78 = function() { return "exportedTo"; }; + var peg$f79 = function() { return "exchangeRate"; }; + var peg$f80 = function() { return "reimbursableTotal"; }; + var peg$f81 = function() { return "nonReimbursableTotal"; }; + var peg$f82 = function() { return "groupFrom"; }; + var peg$f83 = function() { return "groupExpenses"; }; + var peg$f84 = function() { return "groupTotal"; }; + var peg$f85 = function() { return "groupCard"; }; + var peg$f86 = function() { return "groupFeed"; }; + var peg$f87 = function() { return "groupBankAccount"; }; + var peg$f88 = function() { return "groupWithdrawn"; }; + var peg$f89 = function() { return "groupWithdrawalID"; }; + var peg$f90 = function() { return "groupAmountDebited"; }; + var peg$f91 = function() { return "groupAmountReimbursed"; }; + var peg$f92 = function() { return "amountDebited"; }; + var peg$f93 = function() { return "amountReimbursed"; }; + var peg$f94 = function() { return "groupCategory"; }; + var peg$f95 = function() { return "groupTag"; }; + var peg$f96 = function() { return "groupMerchant"; }; + var peg$f97 = function() { return "groupmonth"; }; + var peg$f98 = function() { return "groupweek"; }; + var peg$f99 = function() { return "groupyear"; }; + var peg$f100 = function() { return "groupquarter"; }; + var peg$f101 = function() { return "eq"; }; + var peg$f102 = function() { return "neq"; }; + var peg$f103 = function() { return "gte"; }; + var peg$f104 = function() { return "gt"; }; + var peg$f105 = function() { return "lte"; }; + var peg$f106 = function() { return "lt"; }; + var peg$f107 = function(o) { if (nameOperator) { expectingNestedQuote = (o === "eq"); // Use simple parser if no valid operator is found } isColumnsContext = false; return o; }; - var peg$f107 = function(chars) { return chars.join("").trim(); }; - var peg$f108 = function() { + var peg$f108 = function(c) { return c; }; + var peg$f109 = function(chars) { return chars.join("").trim(); }; + var peg$f110 = function() { isColumnsContext = false; return "and"; }; - var peg$f109 = function() { return expectingNestedQuote; }; - var peg$f110 = function(start, inner, end) { //handle no-breaking space + var peg$f111 = function() { return expectingNestedQuote; }; + var peg$f112 = function(start, inner, end) { //handle no-breaking space return [...start, '"', ...inner, '"', ...end].join(""); }; - var peg$f111 = function(start) {return "“"}; - var peg$f112 = function(start) {return "”"}; - var peg$f113 = function(start) {return "\""}; - var peg$f114 = function(start, inner, end) { + var peg$f113 = function(start) {return "“"}; + var peg$f114 = function(start) {return "”"}; + var peg$f115 = function(start) {return "\""}; + var peg$f116 = function(start, inner, end) { return [...start, '"', ...inner, '"'].join(""); }; var peg$currPos = options.peg$currPos | 0; @@ -1249,16 +1255,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; @@ -1270,10 +1290,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; @@ -1302,7 +1319,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; @@ -1324,7 +1341,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f7(); + s1 = peg$f8(); } s0 = s1; @@ -1344,7 +1361,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f8(); + s1 = peg$f9(); } s0 = s1; @@ -1364,7 +1381,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f9(); + s1 = peg$f10(); } s0 = s1; @@ -1384,7 +1401,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f10(); + s1 = peg$f11(); } s0 = s1; @@ -1412,7 +1429,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f11(); + s1 = peg$f12(); } s0 = s1; } @@ -1433,7 +1450,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f12(); + s1 = peg$f13(); } s0 = s1; @@ -1453,7 +1470,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f13(); + s1 = peg$f14(); } s0 = s1; @@ -1473,7 +1490,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f14(); + s1 = peg$f15(); } s0 = s1; @@ -1501,7 +1518,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f15(); + s1 = peg$f16(); } s0 = s1; } @@ -1522,7 +1539,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f16(); + s1 = peg$f17(); } s0 = s1; @@ -1542,7 +1559,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f17(); + s1 = peg$f18(); } s0 = s1; @@ -1562,7 +1579,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f18(); + s1 = peg$f19(); } s0 = s1; @@ -1582,7 +1599,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f19(); + s1 = peg$f20(); } s0 = s1; @@ -1602,7 +1619,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f20(); + s1 = peg$f21(); } s0 = s1; @@ -1630,7 +1647,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f21(); + s1 = peg$f22(); } s0 = s1; } @@ -1659,7 +1676,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f22(); + s1 = peg$f23(); } s0 = s1; } @@ -1688,7 +1705,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f23(); + s1 = peg$f24(); } s0 = s1; } @@ -1709,7 +1726,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f24(); + s1 = peg$f25(); } s0 = s1; @@ -1729,7 +1746,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f25(); + s1 = peg$f26(); } s0 = s1; @@ -1757,7 +1774,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f26(); + s1 = peg$f27(); } s0 = s1; } @@ -1786,7 +1803,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f27(); + s1 = peg$f28(); } s0 = s1; } @@ -1815,7 +1832,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f28(); + s1 = peg$f29(); } s0 = s1; } @@ -1844,7 +1861,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f29(); + s1 = peg$f30(); } s0 = s1; } @@ -1873,7 +1890,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f30(); + s1 = peg$f31(); } s0 = s1; } @@ -1902,7 +1919,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f31(); + s1 = peg$f32(); } s0 = s1; } @@ -1923,7 +1940,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f32(); + s1 = peg$f33(); } s0 = s1; @@ -1943,7 +1960,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f33(); + s1 = peg$f34(); } s0 = s1; @@ -1963,7 +1980,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f34(); + s1 = peg$f35(); } s0 = s1; @@ -1983,7 +2000,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f35(); + s1 = peg$f36(); } s0 = s1; @@ -2011,7 +2028,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f36(); + s1 = peg$f37(); } s0 = s1; } @@ -2040,7 +2057,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f37(); + s1 = peg$f38(); } s0 = s1; } @@ -2069,7 +2086,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f38(); + s1 = peg$f39(); } s0 = s1; } @@ -2090,7 +2107,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f39(); + s1 = peg$f40(); } s0 = s1; @@ -2110,7 +2127,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f40(); + s1 = peg$f41(); } s0 = s1; @@ -2130,7 +2147,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f41(); + s1 = peg$f42(); } s0 = s1; @@ -2150,7 +2167,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f42(); + s1 = peg$f43(); } s0 = s1; @@ -2170,7 +2187,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f43(); + s1 = peg$f44(); } s0 = s1; @@ -2190,7 +2207,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f44(); + s1 = peg$f45(); } s0 = s1; @@ -2218,7 +2235,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f45(); + s1 = peg$f46(); } s0 = s1; } @@ -2239,7 +2256,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f46(); + s1 = peg$f47(); } s0 = s1; @@ -2259,7 +2276,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f47(); + s1 = peg$f48(); } s0 = s1; @@ -2279,7 +2296,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f48(); + s1 = peg$f49(); } s0 = s1; @@ -2307,7 +2324,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f49(); + s1 = peg$f50(); } s0 = s1; } @@ -2336,7 +2353,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f50(); + s1 = peg$f51(); } s0 = s1; } @@ -2365,7 +2382,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f51(); + s1 = peg$f52(); } s0 = s1; } @@ -2386,7 +2403,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f52(); + s1 = peg$f53(); } s0 = s1; @@ -2414,7 +2431,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f53(); + s1 = peg$f54(); } s0 = s1; } @@ -2435,7 +2452,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f54(); + s1 = peg$f55(); } s0 = s1; @@ -2455,7 +2472,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f55(); + s1 = peg$f56(); } s0 = s1; @@ -2475,7 +2492,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f56(); + s1 = peg$f57(); } s0 = s1; @@ -2495,7 +2512,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f57(); + s1 = peg$f58(); } s0 = s1; @@ -2523,7 +2540,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f58(); + s1 = peg$f59(); } s0 = s1; } @@ -2552,7 +2569,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f59(); + s1 = peg$f60(); } s0 = s1; } @@ -2581,7 +2598,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f60(); + s1 = peg$f61(); } s0 = s1; } @@ -2610,7 +2627,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f61(); + s1 = peg$f62(); } s0 = s1; } @@ -2631,7 +2648,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f62(); + s1 = peg$f63(); } s0 = s1; @@ -2651,7 +2668,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f63(); + s1 = peg$f64(); } s0 = s1; @@ -2663,7 +2680,7 @@ function peg$parse(input, options) { s0 = peg$currPos; peg$savedPos = peg$currPos; - s1 = peg$f64(); + s1 = peg$f65(); if (s1) { s1 = undefined; } else { @@ -2814,7 +2831,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; @@ -2860,7 +2877,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; @@ -2897,7 +2914,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; @@ -2934,7 +2951,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; @@ -2971,7 +2988,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; @@ -3008,7 +3025,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; @@ -3045,7 +3062,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; @@ -3082,7 +3099,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; @@ -3119,7 +3136,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; @@ -3156,7 +3173,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; @@ -3193,7 +3210,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; @@ -3256,7 +3273,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; @@ -3287,7 +3304,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; @@ -3325,7 +3342,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; @@ -3362,7 +3379,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; @@ -3399,7 +3416,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; @@ -3436,7 +3453,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; @@ -3473,7 +3490,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; @@ -3510,7 +3527,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; @@ -3547,7 +3564,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; @@ -3584,7 +3601,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; @@ -3621,7 +3638,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; @@ -3658,7 +3675,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; @@ -3695,7 +3712,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; @@ -3732,7 +3749,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; @@ -3769,7 +3786,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; @@ -3806,7 +3823,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; @@ -3843,7 +3860,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; @@ -3880,7 +3897,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; @@ -3917,7 +3934,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; @@ -3954,7 +3971,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; @@ -3991,7 +4008,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f96(); + s0 = peg$f97(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4028,7 +4045,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f97(); + s0 = peg$f98(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4065,7 +4082,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f98(); + s0 = peg$f99(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4102,7 +4119,7 @@ function peg$parse(input, options) { } if (s2 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f99(); + s0 = peg$f100(); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4129,7 +4146,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) { @@ -4143,7 +4160,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f101(); + s1 = peg$f102(); } s0 = s1; if (s0 === peg$FAILED) { @@ -4157,7 +4174,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f102(); + s1 = peg$f103(); } s0 = s1; if (s0 === peg$FAILED) { @@ -4171,7 +4188,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f103(); + s1 = peg$f104(); } s0 = s1; if (s0 === peg$FAILED) { @@ -4185,7 +4202,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f104(); + s1 = peg$f105(); } s0 = s1; if (s0 === peg$FAILED) { @@ -4199,7 +4216,7 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f105(); + s1 = peg$f106(); } s0 = s1; } @@ -4223,35 +4240,75 @@ function peg$parse(input, options) { s1 = peg$parseoperator(); if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f106(s1); + s1 = peg$f107(s1); } s0 = s1; return s0; } + function peg$parseescapedChar() { + var s0, s1, s2; + + s0 = peg$currPos; + if (input.charCodeAt(peg$currPos) === 92) { + s1 = peg$c117; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e122); } + } + 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$e123); } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f108(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$e123); } + 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$e125); } + } } 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$e123); } + 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$e125); } + } } } } else { @@ -4259,13 +4316,13 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f107(s1); + s1 = peg$f109(s1); } s0 = s1; 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; @@ -4277,7 +4334,7 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = peg$parse_(); peg$savedPos = s0; - s1 = peg$f108(); + s1 = peg$f110(); s0 = s1; return s0; @@ -4289,25 +4346,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$e125); } + if (peg$silentFails === 0) { peg$fail(peg$e127); } } 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$e125); } + if (peg$silentFails === 0) { peg$fail(peg$e127); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e124); } + if (peg$silentFails === 0) { peg$fail(peg$e126); } return s0; } @@ -4317,7 +4374,7 @@ function peg$parse(input, options) { s0 = peg$currPos; peg$savedPos = peg$currPos; - s1 = peg$f109(); + s1 = peg$f111(); if (s1) { s1 = undefined; } else { @@ -4350,76 +4407,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$e127); } + if (peg$silentFails === 0) { peg$fail(peg$e129); } } 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$e127); } + if (peg$silentFails === 0) { peg$fail(peg$e129); } } } 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$e128); } + if (peg$silentFails === 0) { peg$fail(peg$e130); } } 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$e129); } - } - 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$e129); } + if (peg$silentFails === 0) { peg$fail(peg$e131); } + } + } + 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$e131); } + } } } 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$e128); } + if (peg$silentFails === 0) { peg$fail(peg$e130); } } 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$e123); } + if (peg$silentFails === 0) { peg$fail(peg$e125); } } 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$e123); } + if (peg$silentFails === 0) { peg$fail(peg$e125); } } } peg$savedPos = s0; - s0 = peg$f110(s1, s3, s5); + s0 = peg$f112(s1, s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4431,7 +4494,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e126); } + if (peg$silentFails === 0) { peg$fail(peg$e128); } } return s0; @@ -4444,68 +4507,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$e127); } + if (peg$silentFails === 0) { peg$fail(peg$e129); } } 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$e127); } + if (peg$silentFails === 0) { peg$fail(peg$e129); } } } 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$e128); } + if (peg$silentFails === 0) { peg$fail(peg$e130); } } 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$e129); } - } + 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$c117; - 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; - } + 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$e131); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -4520,16 +4554,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$c118; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e131); } + if (peg$silentFails === 0) { peg$fail(peg$e132); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f112(s1); + s4 = peg$f113(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -4551,16 +4585,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$c119; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e132); } + if (peg$silentFails === 0) { peg$fail(peg$e133); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f113(s1); + s4 = peg$f114(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -4569,48 +4603,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$c120; + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e134); } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s4; + s4 = peg$f115(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$e129); } - } + 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$c117; - 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; - } + 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$e131); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -4625,16 +4662,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$c118; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e131); } + if (peg$silentFails === 0) { peg$fail(peg$e132); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f112(s1); + s4 = peg$f113(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -4656,16 +4693,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$c119; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e132); } + if (peg$silentFails === 0) { peg$fail(peg$e133); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; - s4 = peg$f113(s1); + s4 = peg$f114(s1); } else { peg$currPos = s4; s4 = peg$FAILED; @@ -4674,6 +4711,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$c120; + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e134); } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s4; + s4 = peg$f115(s1); + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } } } } @@ -4681,7 +4750,7 @@ function peg$parse(input, options) { s4 = peg$parseclosingQuote(); if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f114(s1, s3, s4); + s0 = peg$f116(s1, s3, s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4693,7 +4762,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e126); } + if (peg$silentFails === 0) { peg$fail(peg$e128); } } return s0; @@ -4704,11 +4773,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$e128); } + if (peg$silentFails === 0) { peg$fail(peg$e130); } } if (s1 !== peg$FAILED) { s2 = peg$currPos; @@ -4742,56 +4811,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$e133); } + if (peg$silentFails === 0) { peg$fail(peg$e135); } } 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$e133); } + if (peg$silentFails === 0) { peg$fail(peg$e135); } } } 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$e134); } + if (peg$silentFails === 0) { peg$fail(peg$e136); } } 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$e134); } + if (peg$silentFails === 0) { peg$fail(peg$e136); } } } 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$e135); } + if (peg$silentFails === 0) { peg$fail(peg$e137); } } 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$e135); } + if (peg$silentFails === 0) { peg$fail(peg$e137); } } } s4 = peg$parseoperator(); @@ -4806,20 +4875,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$e136); } + if (peg$silentFails === 0) { peg$fail(peg$e138); } } 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$e136); } + if (peg$silentFails === 0) { peg$fail(peg$e138); } } } s2 = peg$currPos; @@ -4862,11 +4931,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$e137); } + if (peg$silentFails === 0) { peg$fail(peg$e139); } } } } @@ -4878,11 +4947,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$e138); } + if (peg$silentFails === 0) { peg$fail(peg$e140); } } if (s0 === peg$FAILED) { s0 = peg$currPos; diff --git a/src/libs/SearchParser/autocompleteParser.peggy b/src/libs/SearchParser/autocompleteParser.peggy index 1218bb74a26f..3f6340b79b1a 100644 --- a/src/libs/SearchParser/autocompleteParser.peggy +++ b/src/libs/SearchParser/autocompleteParser.peggy @@ -140,26 +140,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 d4b8746f4014..6393d5c71a02 100644 --- a/src/libs/SearchParser/baseRules.peggy +++ b/src/libs/SearchParser/baseRules.peggy @@ -198,7 +198,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; @@ -213,13 +217,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 be719c9c663b..841ba14ddac2 100644 --- a/src/libs/SearchParser/searchParser.js +++ b/src/libs/SearchParser/searchParser.js @@ -301,24 +301,26 @@ function peg$parse(input, options) { var peg$c114 = ">"; var peg$c115 = "<="; var peg$c116 = "<"; - var peg$c117 = "\u201C"; - var peg$c118 = "\u201D"; - var peg$c119 = "\""; + var peg$c117 = "\\"; + var peg$c118 = "\u201C"; + var peg$c119 = "\u201D"; + var peg$c120 = "\""; 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); @@ -444,23 +446,25 @@ function peg$parse(input, options) { var peg$e121 = peg$literalExpectation(">", false); var peg$e122 = peg$literalExpectation("<=", false); var peg$e123 = peg$literalExpectation("<", false); - var peg$e124 = peg$otherExpectation("word"); - var peg$e125 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e126 = peg$otherExpectation("whitespace"); - var peg$e127 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); - var peg$e128 = peg$otherExpectation("quote"); - var peg$e129 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); - var peg$e130 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); - var peg$e131 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); - var peg$e132 = peg$literalExpectation("\u201C", false); - var peg$e133 = peg$literalExpectation("\u201D", false); - var peg$e134 = peg$literalExpectation("\"", false); - var peg$e135 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e136 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); - var peg$e137 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); - var peg$e138 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); - var peg$e139 = peg$classExpectation([","], false, false); - var peg$e140 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ","], false, false); + var peg$e124 = peg$literalExpectation("\\", false); + var peg$e125 = peg$classExpectation([",", "\"", "\u201D", "\u201C", "\\"], false, false); + var peg$e126 = peg$otherExpectation("word"); + var peg$e127 = peg$classExpectation([" ", ",", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e128 = peg$otherExpectation("whitespace"); + var peg$e129 = peg$classExpectation([" ", "\t", "\r", "\n", "\xA0"], false, false); + var peg$e130 = peg$otherExpectation("quote"); + var peg$e131 = peg$classExpectation([" ", ",", "\"", "\u201D", "\u201C", "\t", "\n", "\r", "\xA0"], true, false); + var peg$e132 = peg$classExpectation(["\"", ["\u201C", "\u201D"]], false, false); + var peg$e133 = peg$classExpectation(["\"", "\u201D", "\u201C", "\r", "\n"], true, false); + var peg$e134 = peg$literalExpectation("\u201C", false); + var peg$e135 = peg$literalExpectation("\u201D", false); + var peg$e136 = peg$literalExpectation("\"", false); + var peg$e137 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e138 = peg$classExpectation([["a", "z"], ["A", "Z"], ["0", "9"]], false, false); + var peg$e139 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0"], false, false); + var peg$e140 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ["a", "z"], ["A", "Z"]], false, false); + var peg$e141 = peg$classExpectation([","], false, false); + var peg$e142 = peg$classExpectation([" ", "\t", "\n", "\r", "\xA0", ","], false, false); var peg$f0 = function(filters) { return applyDefaults(filters); }; var peg$f1 = function(head, tail) { @@ -656,19 +660,20 @@ function peg$parse(input, options) { isColumnsContext = false; return o; }; - var peg$f108 = function(chars) { return chars.join("").trim(); }; - var peg$f109 = function() { + var peg$f108 = function(c) { return c; }; + var peg$f109 = function(chars) { return chars.join("").trim(); }; + var peg$f110 = function() { isColumnsContext = false; return "and"; }; - var peg$f110 = function() { return expectingNestedQuote; }; - var peg$f111 = function(start, inner, end) { //handle no-breaking space + var peg$f111 = function() { return expectingNestedQuote; }; + var peg$f112 = function(start, inner, end) { //handle no-breaking space return [...start, '"', ...inner, '"', ...end].join(""); }; - var peg$f112 = function(start) {return "“"}; - var peg$f113 = function(start) {return "”"}; - var peg$f114 = function(start) {return "\""}; - var peg$f115 = function(start, inner, end) { + var peg$f113 = function(start) {return "“"}; + var peg$f114 = function(start) {return "”"}; + var peg$f115 = function(start) {return "\""}; + var peg$f116 = function(start, inner, end) { return [...start, '"', ...inner, '"'].join(""); }; var peg$currPos = options.peg$currPos | 0; @@ -949,22 +954,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 { @@ -4414,28 +4425,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$c117; + peg$currPos++; + } else { + s1 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e124); } + } + 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$e125); } + } + if (s2 !== peg$FAILED) { + peg$savedPos = s0; + s0 = peg$f108(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$e125); } + 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$e127); } + } } 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$e125); } + 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$e127); } + } } } } else { @@ -4443,13 +4494,13 @@ function peg$parse(input, options) { } if (s1 !== peg$FAILED) { peg$savedPos = s0; - s1 = peg$f108(s1); + s1 = peg$f109(s1); } s0 = s1; 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; @@ -4461,7 +4512,7 @@ function peg$parse(input, options) { s0 = peg$currPos; s1 = peg$parse_(); peg$savedPos = s0; - s1 = peg$f109(); + s1 = peg$f110(); s0 = s1; return s0; @@ -4473,25 +4524,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$e127); } + if (peg$silentFails === 0) { peg$fail(peg$e129); } } 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$e127); } + if (peg$silentFails === 0) { peg$fail(peg$e129); } } } peg$silentFails--; s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e126); } + if (peg$silentFails === 0) { peg$fail(peg$e128); } return s0; } @@ -4501,7 +4552,7 @@ function peg$parse(input, options) { s0 = peg$currPos; peg$savedPos = peg$currPos; - s1 = peg$f110(); + s1 = peg$f111(); if (s1) { s1 = undefined; } else { @@ -4534,76 +4585,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$e129); } + if (peg$silentFails === 0) { peg$fail(peg$e131); } } 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$e129); } + if (peg$silentFails === 0) { peg$fail(peg$e131); } } } 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$e130); } + if (peg$silentFails === 0) { peg$fail(peg$e132); } } 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$e131); } - } - 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$e131); } + if (peg$silentFails === 0) { peg$fail(peg$e133); } + } + } + 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$e133); } + } } } 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$e130); } + if (peg$silentFails === 0) { peg$fail(peg$e132); } } 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$e125); } + if (peg$silentFails === 0) { peg$fail(peg$e127); } } 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$e125); } + if (peg$silentFails === 0) { peg$fail(peg$e127); } } } peg$savedPos = s0; - s0 = peg$f111(s1, s3, s5); + s0 = peg$f112(s1, s3, s5); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4615,7 +4672,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e128); } + if (peg$silentFails === 0) { peg$fail(peg$e130); } } return s0; @@ -4628,68 +4685,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$e129); } + if (peg$silentFails === 0) { peg$fail(peg$e131); } } 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$e129); } + if (peg$silentFails === 0) { peg$fail(peg$e131); } } } 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$e130); } + if (peg$silentFails === 0) { peg$fail(peg$e132); } } 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$e131); } - } + 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$c117; - peg$currPos++; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e132); } - } - if (s6 !== peg$FAILED) { - peg$savedPos = s4; - s4 = peg$f112(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$e133); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -4704,12 +4732,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$c118; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e133); } + if (peg$silentFails === 0) { peg$fail(peg$e134); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; @@ -4735,12 +4763,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$c119; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e134); } + if (peg$silentFails === 0) { peg$fail(peg$e135); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; @@ -4753,48 +4781,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$c120; + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e136); } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s4; + s4 = peg$f115(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$e131); } - } + 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$c117; - peg$currPos++; - } else { - s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e132); } - } - if (s6 !== peg$FAILED) { - peg$savedPos = s4; - s4 = peg$f112(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$e133); } } if (s4 === peg$FAILED) { s4 = peg$currPos; @@ -4809,12 +4840,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$c118; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e133); } + if (peg$silentFails === 0) { peg$fail(peg$e134); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; @@ -4840,12 +4871,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$c119; peg$currPos++; } else { s6 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e134); } + if (peg$silentFails === 0) { peg$fail(peg$e135); } } if (s6 !== peg$FAILED) { peg$savedPos = s4; @@ -4858,6 +4889,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$c120; + peg$currPos++; + } else { + s6 = peg$FAILED; + if (peg$silentFails === 0) { peg$fail(peg$e136); } + } + if (s6 !== peg$FAILED) { + peg$savedPos = s4; + s4 = peg$f115(s1); + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } else { + peg$currPos = s4; + s4 = peg$FAILED; + } + } } } } @@ -4865,7 +4928,7 @@ function peg$parse(input, options) { s4 = peg$parseclosingQuote(); if (s4 !== peg$FAILED) { peg$savedPos = s0; - s0 = peg$f115(s1, s3, s4); + s0 = peg$f116(s1, s3, s4); } else { peg$currPos = s0; s0 = peg$FAILED; @@ -4877,7 +4940,7 @@ function peg$parse(input, options) { peg$silentFails--; if (s0 === peg$FAILED) { s1 = peg$FAILED; - if (peg$silentFails === 0) { peg$fail(peg$e128); } + if (peg$silentFails === 0) { peg$fail(peg$e130); } } return s0; @@ -4888,11 +4951,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$e130); } + if (peg$silentFails === 0) { peg$fail(peg$e132); } } if (s1 !== peg$FAILED) { s2 = peg$currPos; @@ -4926,56 +4989,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$e135); } + if (peg$silentFails === 0) { peg$fail(peg$e137); } } 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$e135); } + if (peg$silentFails === 0) { peg$fail(peg$e137); } } } 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$e136); } + if (peg$silentFails === 0) { peg$fail(peg$e138); } } 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$e136); } + if (peg$silentFails === 0) { peg$fail(peg$e138); } } } 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$e137); } + if (peg$silentFails === 0) { peg$fail(peg$e139); } } 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$e137); } + if (peg$silentFails === 0) { peg$fail(peg$e139); } } } s4 = peg$parseoperator(); @@ -4990,20 +5053,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$e138); } + if (peg$silentFails === 0) { peg$fail(peg$e140); } } 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$e138); } + if (peg$silentFails === 0) { peg$fail(peg$e140); } } } s2 = peg$currPos; @@ -5046,11 +5109,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$e139); } + if (peg$silentFails === 0) { peg$fail(peg$e141); } } } } @@ -5062,11 +5125,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$e140); } + if (peg$silentFails === 0) { peg$fail(peg$e142); } } if (s0 === peg$FAILED) { s0 = peg$currPos; diff --git a/src/libs/SearchParser/searchParser.peggy b/src/libs/SearchParser/searchParser.peggy index 4022a286ad3a..639a8b5f511c 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/src/libs/SearchQueryUtils.ts b/src/libs/SearchQueryUtils.ts index d76856248cf2..bd1976054406 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. 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) { - if (str.includes(' ') || str.includes(`\xA0`) || (shouldQuoteComma && str.includes(','))) { - return `"${str}"`; +function sanitizeSearchValue(str: string) { + const escaped = str.replaceAll(/[\\"“”]/g, '\\$&'); + if (escaped.includes(' ') || escaped.includes(`\xA0`) || 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,13 +457,28 @@ function getFilterFromQuery(queryJSON: SearchQueryJSON | undefined, filterKey: S return {value, isNegated}; } +/** + * 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 lowerCaseValue = value.toLowerCase(); + const matches = Object.values(policies ?? {}).filter((policy) => policy?.name?.toLowerCase() === lowerCaseValue); + 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) { +function getUpdatedFilterValue(filterName: SyntaxFilterKey, filterValue: string | string[], shouldSkipAmountConversion = false, policies?: OnyxCollection) { if (AMOUNT_FILTER_KEYS.includes(filterName as SearchAmountFilterKeys)) { if (shouldSkipAmountConversion) { return filterValue; @@ -493,6 +507,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, policies); + } + return filterValue.map((value) => resolvePolicyIDFromName(value, policies)); + } + if (filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.REPORT_ID || filterName === CONST.SEARCH.SYNTAX_FILTER_KEYS.WITHDRAWAL_ID) { const cleanIDs = (value: string) => value @@ -2183,7 +2204,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) { @@ -2191,7 +2212,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; @@ -2391,6 +2412,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, @@ -2414,7 +2436,7 @@ function buildFilterQueryWithSortDefaults( return queryString; } - return getQueryWithUpdatedValues(queryString, true); + return getQueryWithUpdatedValues(queryString, true, policies); } /** @@ -2617,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 9ec0f3851eec..0e23465028f6 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'; @@ -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: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', () => { + 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', () => { @@ -2411,6 +2420,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'); @@ -4150,20 +4170,54 @@ 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('Globex,Ltd')).toBe('"Globex,Ltd"'); + }); + + 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')).toBe('"Acme \\"US\\",Inc"'); + expect(sanitizeSearchValue('Acme “US” Inc')).toBe('"Acme \\“US\\” Inc"'); + }); + + it('serializes a value with no character needing escaping exactly as before', () => { + expect(sanitizeSearchValue('Acme, Inc.')).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('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 a value without quotes untouched', () => { - expect(stripSearchValueQuotes('Acme, Inc.')).toBe('Acme, Inc.'); + 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 36bedc47ba34..2d3a6b8bae56 100644 --- a/tests/unit/SearchAutocompleteParserTest.ts +++ b/tests/unit/SearchAutocompleteParserTest.ts @@ -1,5 +1,6 @@ import type {SearchAutocompleteQueryRange, SearchAutocompleteResult} from '@components/Search/types'; +import {parseForAutocomplete} from '@libs/SearchAutocompleteUtils'; import {parse} from '@libs/SearchParser/autocompleteParser'; import parserCommonTests from '../utils/fixtures/searchParsersCommonQueries'; @@ -796,3 +797,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:Globex\\,Ltd', 'policyID', 'Globex,Ltd'], + ])('reads %s back as a single value', (query, key, value) => { + const ranges = parseForAutocomplete(query)?.ranges ?? []; + + expect(ranges.filter((range) => range.key === key).map((range) => range.value)).toEqual([value]); + }); + + test.each([ + ['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) => { + 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 = 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"'); + }); +}); 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 812469f122e0..b27ef7840d88 100644 --- a/tests/unit/SearchRouterUtilsTest.ts +++ b/tests/unit/SearchRouterUtilsTest.ts @@ -210,22 +210,7 @@ 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('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, @@ -236,11 +221,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, @@ -249,8 +234,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}`); });