tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

autocomplete.js (5154B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 "use strict";
      5 
      6 const {
      7  AUTOCOMPLETE_CLEAR,
      8  AUTOCOMPLETE_DATA_RECEIVE,
      9  AUTOCOMPLETE_PENDING_REQUEST,
     10  AUTOCOMPLETE_RETRIEVE_FROM_CACHE,
     11  EVALUATE_EXPRESSION,
     12  UPDATE_HISTORY_POSITION,
     13  REVERSE_SEARCH_INPUT_CHANGE,
     14  REVERSE_SEARCH_BACK,
     15  REVERSE_SEARCH_NEXT,
     16  WILL_NAVIGATE,
     17 } = require("resource://devtools/client/webconsole/constants.js");
     18 
     19 function getDefaultState(overrides = {}) {
     20  return Object.freeze({
     21    cache: null,
     22    matches: [],
     23    matchProp: null,
     24    isElementAccess: false,
     25    pendingRequestId: null,
     26    isUnsafeGetter: false,
     27    getterPath: null,
     28    authorizedEvaluations: [],
     29    ...overrides,
     30  });
     31 }
     32 
     33 function autocomplete(state = getDefaultState(), action) {
     34  switch (action.type) {
     35    case WILL_NAVIGATE:
     36      return getDefaultState();
     37    case AUTOCOMPLETE_RETRIEVE_FROM_CACHE:
     38      return autoCompleteRetrieveFromCache(state, action);
     39    case AUTOCOMPLETE_PENDING_REQUEST:
     40      return {
     41        ...state,
     42        cache: null,
     43        pendingRequestId: action.id,
     44      };
     45    case AUTOCOMPLETE_DATA_RECEIVE:
     46      if (action.id !== state.pendingRequestId) {
     47        return state;
     48      }
     49 
     50      if (action.data.matches === null) {
     51        return getDefaultState();
     52      }
     53 
     54      if (action.data.isUnsafeGetter) {
     55        // We only want to display the getter confirm popup if the last char is a dot or
     56        // an opening bracket, or if the user forced the autocompletion with Ctrl+Space.
     57        if (
     58          action.input.endsWith(".") ||
     59          action.input.endsWith("[") ||
     60          action.force
     61        ) {
     62          return {
     63            ...getDefaultState(),
     64            isUnsafeGetter: true,
     65            getterPath: action.data.getterPath,
     66            authorizedEvaluations: action.authorizedEvaluations,
     67          };
     68        }
     69 
     70        return {
     71          ...state,
     72          pendingRequestId: null,
     73        };
     74      }
     75 
     76      return {
     77        ...state,
     78        authorizedEvaluations: action.authorizedEvaluations,
     79        getterPath: null,
     80        isUnsafeGetter: false,
     81        pendingRequestId: null,
     82        cache: {
     83          input: action.input,
     84          frameActorId: action.frameActorId,
     85          ...action.data,
     86        },
     87        ...action.data,
     88      };
     89    // Reset the autocomplete data when:
     90    // - clear is explicitely called
     91    // - the user navigates the history
     92    // - or an expression was evaluated.
     93    case AUTOCOMPLETE_CLEAR:
     94      return getDefaultState({
     95        authorizedEvaluations: state.authorizedEvaluations,
     96      });
     97    case EVALUATE_EXPRESSION:
     98    case UPDATE_HISTORY_POSITION:
     99    case REVERSE_SEARCH_INPUT_CHANGE:
    100    case REVERSE_SEARCH_BACK:
    101    case REVERSE_SEARCH_NEXT:
    102      return getDefaultState();
    103  }
    104 
    105  return state;
    106 }
    107 
    108 /**
    109 * Retrieve from cache action reducer.
    110 *
    111 * @param {object} state
    112 * @param {object} action
    113 * @returns {object} new state.
    114 */
    115 function autoCompleteRetrieveFromCache(state, action) {
    116  const { input } = action;
    117  const { cache } = state;
    118 
    119  let filterBy = input;
    120  if (cache.isElementAccess) {
    121    // if we're performing an element access, we can simply retrieve whatever comes
    122    // after the last opening bracket.
    123    filterBy = input.substring(input.lastIndexOf("[") + 1);
    124  } else {
    125    // Find the last non-alphanumeric other than "_", ":", or "$" if it exists.
    126    const lastNonAlpha = input.match(/[^a-zA-Z0-9_$:][a-zA-Z0-9_$:]*$/);
    127    // If input contains non-alphanumerics, use the part after the last one
    128    // to filter the cache.
    129    if (lastNonAlpha) {
    130      filterBy = input.substring(input.lastIndexOf(lastNonAlpha) + 1);
    131    }
    132  }
    133  const stripWrappingQuotes = s =>
    134    s.replace(/^['"`](.+(?=['"`]$))['"`]$/g, "$1");
    135  const filterByLc = filterBy.toLocaleLowerCase();
    136  const looseMatching =
    137    !filterBy || filterBy[0].toLocaleLowerCase() === filterBy[0];
    138  const needStripQuote = cache.isElementAccess && !/^[`"']/.test(filterBy);
    139  const newList = cache.matches.filter(l => {
    140    if (needStripQuote) {
    141      l = stripWrappingQuotes(l);
    142    }
    143 
    144    if (looseMatching) {
    145      return l.toLocaleLowerCase().startsWith(filterByLc);
    146    }
    147 
    148    return l.startsWith(filterBy);
    149  });
    150 
    151  newList.sort((a, b) => {
    152    const startingQuoteRegex = /^('|"|`)/;
    153    const aFirstMeaningfulChar = startingQuoteRegex.test(a) ? a[1] : a[0];
    154    const bFirstMeaningfulChar = startingQuoteRegex.test(b) ? b[1] : b[0];
    155    const lA =
    156      aFirstMeaningfulChar.toLocaleLowerCase() === aFirstMeaningfulChar;
    157    const lB =
    158      bFirstMeaningfulChar.toLocaleLowerCase() === bFirstMeaningfulChar;
    159    if (lA === lB) {
    160      if (a === filterBy) {
    161        return -1;
    162      }
    163      if (b === filterBy) {
    164        return 1;
    165      }
    166      return a.localeCompare(b);
    167    }
    168    return lA ? -1 : 1;
    169  });
    170 
    171  return {
    172    ...state,
    173    isUnsafeGetter: false,
    174    getterPath: null,
    175    matches: newList,
    176    matchProp: filterBy,
    177    isElementAccess: cache.isElementAccess,
    178  };
    179 }
    180 
    181 exports.autocomplete = autocomplete;