PageSourceSearch

https://remotive.com/remotive_website_job/static/refactoring/js/v2/searchRouter.js?v=3

js remotive.com collected 2026-09-25 04:08:17 UTC 22,525 bytes, 664 lines download raw bytes

1function normalizePath(path) {
2return (path || '').replace(/^\/+|\/+$/g, '');
3}
4
5// Basic sanitizer for URL-derived values (query and facet-like params).
6// This is a defensive layer to prevent HTML/JS injection via query string.
7// We strip angle brackets which are required to start HTML tags; normal
8// job searches and facet values should not rely on them.
9function sanitizeQueryParam(value) {
10  if (value == null) return '';
11  try {
12    return String(value).replace(/[<>]/g, '');
13  } catch (e) {
14    return '';
15  }
16}
17
18function buildPathWithBase(relativePath) {
19const base = ROUTE_BASE_PATH.replace(/\/+$/g, '');
20const rel = (relativePath || '').replace(/^\/+/, '');
21return rel ? base + '/' + rel : base || '/';
22}
23
24const REFINEMENT_ROUTE_KEYS = [
25  ['category', 'category'],
26  ['location_filters', 'location'],
27  ['occupation', 'occupation'],
28  ['seniority', 'experience'],
29  ['job_type', 'employment-type'],
30  ['lang', 'lang'],
31  ['skills', 'skills'],
32];
33const POSTED_ROUTE_VALUES = ['last-24h', 'last-72h', 'last-7-days', 'last-30-days', 'last-90-days'];
34
35function getMultiValueParam(qs, name) {
36  const vals = qs.getAll(name);
37  const raw = vals.length > 1 ? vals.join(',') : (qs.get(name) || '');
38  return sanitizeQueryParam(raw);
39}
40
41function mapRefinementListToRoute(rl) {
42  const result = {};
43  REFINEMENT_ROUTE_KEYS.forEach(([refinementKey, routeKey]) => {
44    if (rl[refinementKey] && rl[refinementKey].length > 0) {
45      result[routeKey] = rl[refinementKey].join(',');
46    }
47  });
48  return result;
49}
50
51function normalizePresetRefinement(value) {
52  if (value == null) return value;
53  const v = String(value).trim();
54  if (!v) return '';
55  return v;
56}
57
58function normalizePostedValue(value) {
59  const v = sanitizeQueryParam(value || '').trim().toLowerCase();
60  return POSTED_ROUTE_VALUES.indexOf(v) >= 0 ? v : '';
61}
62
63function getServerPresetFilters() {
64  if (window.serverPresetFilters) return window.serverPresetFilters;
65  try {
66    const el = document.getElementById('js_variables');
67    if (!el) return null;
68    const raw = el.getAttribute('data-preset-filters');
69    if (!raw) return null;
70    const parsed = JSON.parse(raw);
71    window.serverPresetFilters = parsed;
72    return parsed;
73  } catch (e) {
74    return null;
75  }
76}
77
78function mergeIndexUiStateWithPreset(indexUiState, preset) {
79  if (!preset || typeof preset !== 'object') return indexUiState;
80  const merged = Object.assign({}, indexUiState);
81  try {
82    if (!merged.query && preset.query) {
83      merged.query = String(preset.query);
84    }
85  } catch (e) {
86  }
87  const ensureRefinementList = () => {
88    if (!merged.refinementList || typeof merged.refinementList !== 'object') {
89      merged.refinementList = {};
90    }
91  };
92  if (preset.refinementList && typeof preset.refinementList === 'object') {
93    ensureRefinementList();
94    merged.refinementList = Object.assign({}, merged.refinementList);
95    Object.keys(preset.refinementList).forEach((attr) => {
96      const rawVals = Array.isArray(preset.refinementList[attr]) ? preset.refinementList[attr] : [preset.refinementList[attr]];
97      const presetVals = rawVals.map((val) => normalizePresetRefinement(val));
98      const currentVals = merged.refinementList && Array.isArray(merged.refinementList[attr]) ? merged.refinementList[attr] : [];
99      if (!currentVals.length) {
100        if (presetVals.length) merged.refinementList[attr] = presetVals.slice();
101      } else {
102        const set = new Set(currentVals);
103        presetVals.forEach(v => set.add(v));
104        merged.refinementList[attr] = Array.from(set);
105      }
106    });
107  }
108  if (!window.initialSkillsOperator && preset.skillsOperator) {
109    window.initialSkillsOperator = String(preset.skillsOperator).toLowerCase() === 'and' ? 'and' : 'or';
110  }
111  const presetPosted = normalizePostedValue(preset.posted);
112  if (!merged.posted && presetPosted) {
113    merged.posted = presetPosted;
114  }
115
116  // Ensure numeric salary presets are reflected in the InstantSearch UI state.
117  // The backend uses `numericRefinements` for SSR, while the UI widgets read `uiState.range.max_salary`.
118  if (preset.range && preset.range.max_salary) {
119    merged.range = Object.assign({}, merged.range || {}, { max_salary: String(preset.range.max_salary) });
120  } else if (preset.numericRefinements && preset.numericRefinements.max_salary) {
121    const nr = preset.numericRefinements.max_salary;
122    if (nr && typeof nr === 'object' && nr['>=']) {
123      const min = parseInt(String(nr['>=']), 10);
124      if (!Number.isNaN(min) && min > 0) {
125        merged.range = Object.assign({}, merged.range || {}, { max_salary: String(min) + ':' });
126      }
127    }
128  }
129  return merged;
130}
131
132
133function routeToState() {
134  const filters = {};
135
136  try {
137    const qs = new URLSearchParams(window.location.search || '');
138    const rawQ = qs.get('query') || qs.get('q');
139    const q = sanitizeQueryParam(rawQ);
140    const routeRefinements = {};
141    REFINEMENT_ROUTE_KEYS.forEach(([refinementKey, routeKey]) => {
142      routeRefinements[refinementKey] = getMultiValueParam(qs, routeKey);
143    });
144    const skillsOp = (qs.get('skillsOperator') || qs.get('skills_op') || '').toLowerCase();
145    const minSalaryRaw = qs.get('min_salary') || qs.get('salary_min') || qs.get('max_salary') || '';
146    const minSalaryStr = sanitizeQueryParam(minSalaryRaw);
147    const postedValue = normalizePostedValue(qs.get('posted') || '');
148
149    if (q) filters.query = q;
150    REFINEMENT_ROUTE_KEYS.forEach(([refinementKey, routeKey]) => {
151      const rawValue = routeRefinements[refinementKey];
152      if (!rawValue) return;
153      const values = rawValue.split(',').filter(Boolean);
154      if (routeKey === 'category' || routeKey === 'occupation') {
155        if (values.length) filters[refinementKey] = values;
156        return;
157      }
158      filters[refinementKey] = values;
159    });
160    if (skillsOp === 'and' || skillsOp === 'or') {
161      window.initialSkillsOperator = skillsOp;
162      filters.skillsOperator = skillsOp;
163    } else if (!window.initialSkillsOperator) {
164      window.initialSkillsOperator = 'or';
165    }
166    if (minSalaryStr) {
167      const parsed = parseInt(minSalaryStr, 10);
168      if (!Number.isNaN(parsed)) {
169        filters.min_salary = String(parsed);
170      }
171    }
172    if (postedValue) {
173      filters.posted = postedValue;
174    }
175  } catch (e) {
176  }
177
178  const indexUiState = {};
179
180  const refinements = {};
181  REFINEMENT_ROUTE_KEYS.forEach(([refinementKey]) => {
182    if (filters[refinementKey]) {
183      refinements[refinementKey] = Array.isArray(filters[refinementKey])
184        ? filters[refinementKey]
185        : [filters[refinementKey]];
186    }
187  });
188  if (Object.keys(refinements).length) {
189    indexUiState.refinementList = refinements;
190  }
191
192  if (filters.query) {
193    indexUiState.query = filters.query;
194  }
195
196  if (filters.skillsOperator) {
197    indexUiState.skillsOperator = filters.skillsOperator;
198  }
199  if (filters.min_salary) {
200    const min = String(filters.min_salary);
201    indexUiState.range = Object.assign({}, indexUiState.range, { max_salary: min + ':' });
202  }
203  if (filters.posted) {
204    indexUiState.posted = filters.posted;
205  }
206
207  const presets = getServerPresetFilters();
208  try {
209    const baselineFromPresets = mergeIndexUiStateWithPreset({}, presets || {});
210    window.serverPresetUiStateBaseline = baselineFromPresets;
211    window.serverPresetFlatRouteStateBaseline = buildRouteStateFromIndexUiState(baselineFromPresets);
212    window.serverPresetSkillsOperatorBaseline = baselineFromPresets.skillsOperator;
213  } catch (e) {
214    window.serverPresetUiStateBaseline = undefined;
215    window.serverPresetFlatRouteStateBaseline = undefined;
216    window.serverPresetSkillsOperatorBaseline = undefined;
217  }
218  const mergedUiState = mergeIndexUiStateWithPreset(indexUiState, presets);
219
220  try {
221    const currentPathNormalized = normalizePath(window.location.pathname);
222    const hasNoRefinements = !(mergedUiState.refinementList && Object.keys(mergedUiState.refinementList).length);
223    if (currentPathNormalized === '' && hasNoRefinements) {
224      window.onVanity = true;
225      window.vanityPath = '/';
226      window.presetCanonicalPath = buildPathFromRouteState().replace(/\/+$/, '');
227    }
228  } catch (e) {
229  }
230
231  if (presets) {
232    window.onVanity = true;
233    window.vanityPath = window.location.pathname || '/';
234    try {
235      window.presetCanonicalPath = buildPathFromRouteState().replace(/\/+$/, '');
236    } catch (e) {
237    }
238  }
239
240  return {
241    remotive_unlimited: mergedUiState,
242  };
243}
244
245
246function stateToRoute(uiState) {
247  const indexUiState = uiState.remotive_unlimited || {};
248
249  const routeState = {};
250
251  const rl = indexUiState.refinementList || {};
252  Object.assign(routeState, mapRefinementListToRoute(rl));
253  if (indexUiState.skillsOperator && (indexUiState.skillsOperator === 'and' || indexUiState.skillsOperator === 'or')) {
254    routeState.skillsOperator = indexUiState.skillsOperator;
255  } else if (typeof window.skillsOperator === 'string' && (window.skillsOperator === 'and' || window.skillsOperator === 'or')) {
256    routeState.skillsOperator = window.skillsOperator;
257  }
258  if (indexUiState.query) {
259    routeState.query = String(indexUiState.query);
260  }
261  if (indexUiState.range && indexUiState.range.max_salary) {
262    try {
263      const rangeStr = String(indexUiState.range.max_salary);
264      const minPart = rangeStr.split(':')[0];
265      const parsed = parseInt(minPart, 10);
266      if (!Number.isNaN(parsed) && parsed > 0) {
267        routeState.min_salary = String(parsed);
268      }
269    } catch (e) {}
270  }
271  if (indexUiState.posted) {
272    const postedValue = normalizePostedValue(indexUiState.posted);
273    if (postedValue) {
274      routeState.posted = postedValue;
275    }
276  }
277  try {
278    window.lastFullRouteStateForVanity = Object.assign({}, routeState);
279    window.currentRouteMatchesPresetBaseline = isCurrentRouteStatePresetBaseline(
280      window.lastFullRouteStateForVanity
281    );
282  } catch (e) {
283    window.lastFullRouteStateForVanity = Object.assign({}, routeState);
284    window.currentRouteMatchesPresetBaseline = false;
285  }
286  return routeState;
287}
288
289function trimRouteStateAgainstBaseline(routeState) {
290  try {
291    const baseline = window.serverPresetUiStateBaseline || {};
292    const baselineRL = (baseline && baseline.refinementList) || {};
293    const trimmed = {};
294    const toArray = (v) => Array.isArray(v) ? v.slice() : (v ? [String(v)] : []);
295    const diffCsv = (currentCsv, baselineArr) => {
296      const currentArr = toArray(currentCsv && String(currentCsv).split(',').filter(Boolean));
297      const baseSet = new Set(toArray(baselineArr));
298      const extras = currentArr.filter(v => !baseSet.has(v));
299      return extras.join(',');
300    };
301
302    REFINEMENT_ROUTE_KEYS.forEach(([refinementKey, routeKey]) => {
303      if (routeState[routeKey]) {
304        const d = diffCsv(routeState[routeKey], baselineRL[refinementKey]);
305        if (d) trimmed[routeKey] = d;
306      }
307    });
308
309    if (routeState.query) {
310      const safeQuery = sanitizeQueryParam(routeState.query);
311      const baseQuery = (baseline && baseline.query) ? String(baseline.query) : '';
312      if (String(safeQuery) !== baseQuery) trimmed.query = String(safeQuery);
313    }
314
315    if (routeState.min_salary) {
316      let baseMin = '';
317      try {
318        if (baseline && baseline.range && baseline.range.max_salary) {
319          baseMin = String(baseline.range.max_salary).split(':')[0] || '';
320        }
321      } catch (e) { baseMin = ''; }
322      if (String(routeState.min_salary) !== String(baseMin)) {
323        trimmed.min_salary = String(routeState.min_salary);
324      }
325    }
326    if (routeState.posted) {
327      const currentPosted = normalizePostedValue(routeState.posted);
328      const basePosted = normalizePostedValue(baseline.posted || '');
329      if (currentPosted && currentPosted !== basePosted) {
330        trimmed.posted = currentPosted;
331      }
332    }
333
334    try {
335      const currOp = (window.skillsOperator || 'or');
336      const baseOp = (window.serverPresetSkillsOperatorBaseline || 'or');
337      if (currOp !== baseOp) {
338        trimmed.skillsOperator = currOp;
339      }
340    } catch (e) {}
341
342    return trimmed;
343  } catch (e) {
344    return routeState || {};
345  }
346}
347
348function buildPathFromRouteState() {
349  return buildPathWithBase('');
350}
351
352function buildSearchFromRouteState(routeState) {
353  try {
354    const sp = new URLSearchParams();
355    const searchableKeys = [
356      'query',
357      'category',
358      'location',
359      'occupation',
360      'experience',
361      'employment-type',
362      'skills',
363      'lang',
364      'min_salary',
365      'posted',
366    ];
367    searchableKeys.forEach((key) => {
368      if (routeState && routeState[key]) {
369        sp.set(key, sanitizeQueryParam(routeState[key]));
370      }
371    });
372    if (routeState && routeState.skillsOperator) {
373      const op = normalizeRouteStateOperator(routeState.skillsOperator);
374      const baselineOp = normalizeRouteStateOperator(
375        window.serverPresetSkillsOperatorBaseline || window.initialSkillsOperator || 'or'
376      );
377      if (op && op !== baselineOp) {
378        sp.set('skillsOperator', op);
379      }
380    }
381    const s = sp.toString();
382    return s ? ('?' + s) : '';
383  } catch (e) {
384    return '';
385  }
386}
387
388function buildRouteStateFromIndexUiState(indexUiState) {
389  const flat = {};
390  const rl = indexUiState.refinementList || {};
391  const routeRefinements = mapRefinementListToRoute(rl);
392  delete routeRefinements.lang;
393  Object.assign(flat, routeRefinements);
394  const op = (indexUiState.skillsOperator || window.initialSkillsOperator || window.skillsOperator || 'or');
395  if (op) flat.skillsOperator = op;
396  const postedValue = normalizePostedValue(indexUiState.posted);
397  if (postedValue) flat.posted = postedValue;
398  return flat;
399}
400
401function parseRouteStateFromURL() {
402  const routeState = {};
403
404  try {
405    const qs = new URLSearchParams(window.location.search || '');
406    const rawQ = qs.get('query') || qs.get('q');
407    const q = sanitizeQueryParam(rawQ);
408    if (q) routeState.query = q;
409    REFINEMENT_ROUTE_KEYS.forEach(([, routeKey]) => {
410      const value = getMultiValueParam(qs, routeKey);
411      if (value) routeState[routeKey] = value;
412    });
413    const skillsOp = (qs.get('skillsOperator') || qs.get('skills_op') || '').toLowerCase();
414    if (skillsOp === 'and' || skillsOp === 'or') routeState.skillsOperator = skillsOp;
415    const minSalary = qs.get('min_salary') || qs.get('salary_min') || qs.get('max_salary');
416    if (minSalary) routeState.min_salary = sanitizeQueryParam(minSalary);
417    const posted = normalizePostedValue(qs.get('posted') || '');
418    if (posted) routeState.posted = posted;
419  } catch (e) {
420  }
421
422  return routeState;
423}
424
425function toSortedCsvArray(value) {
426  if (value == null) return [];
427  return String(value)
428    .split(',')
429    .map((part) => sanitizeQueryParam(part).trim())
430    .filter(Boolean)
431    .sort();
432}
433
434function normalizeRouteStateOperator(value) {
435  const op = String(value || '').toLowerCase();
436  return (op === 'and' || op === 'or') ? op : '';
437}
438
439function parseMinSalaryFromRange(rangeValue) {
440  try {
441    const minPart = String(rangeValue || '').split(':')[0];
442    const parsed = parseInt(minPart, 10);
443    if (!Number.isNaN(parsed) && parsed > 0) {
444      return String(parsed);
445    }
446  } catch (e) {}
447  return '';
448}
449
450function getPresetBaselineRouteState() {
451  if (window.serverPresetComparableRouteState) {
452    return window.serverPresetComparableRouteState;
453  }
454  try {
455    const baselineUi = window.serverPresetUiStateBaseline || {};
456    const baseline = buildRouteStateFromIndexUiState(baselineUi);
457    const baselineRL = (baselineUi && baselineUi.refinementList) || {};
458
459    if (baselineUi.query) baseline.query = sanitizeQueryParam(baselineUi.query);
460    if (baselineRL.lang && baselineRL.lang.length > 0) baseline.lang = baselineRL.lang.join(',');
461    if (baselineUi.range && baselineUi.range.max_salary) {
462      const minSalary = parseMinSalaryFromRange(baselineUi.range.max_salary);
463      if (minSalary) baseline.min_salary = minSalary;
464    }
465    const posted = normalizePostedValue(baselineUi.posted || '');
466    if (posted) baseline.posted = posted;
467    const op = normalizeRouteStateOperator(
468      baselineUi.skillsOperator || window.serverPresetSkillsOperatorBaseline
469    );
470    if (op) baseline.skillsOperator = op;
471
472    window.serverPresetComparableRouteState = baseline;
473    return baseline;
474  } catch (e) {
475    return null;
476  }
477}
478
479function isCurrentRouteStatePresetBaseline(routeState) {
480  const baseline = getPresetBaselineRouteState();
481  if (!baseline) return false;
482
483  const csvKeys = [
484    'category',
485    'location',
486    'occupation',
487    'experience',
488    'employment-type',
489    'skills',
490    'lang',
491  ];
492
493  for (let i = 0; i < csvKeys.length; i++) {
494    const key = csvKeys[i];
495    const currentVals = toSortedCsvArray(routeState && routeState[key]);
496    const baselineVals = toSortedCsvArray(baseline[key]);
497    if (currentVals.join(',') !== baselineVals.join(',')) {
498      return false;
499    }
500  }
501
502  const currentQuery = sanitizeQueryParam(routeState && routeState.query);
503  const baselineQuery = sanitizeQueryParam(baseline.query || '');
504  if (String(currentQuery || '') !== String(baselineQuery || '')) {
505    return false;
506  }
507
508  const currentMinSalary = sanitizeQueryParam(routeState && routeState.min_salary);
509  const baselineMinSalary = sanitizeQueryParam(baseline.min_salary || '');
510  if (String(currentMinSalary || '') !== String(baselineMinSalary || '')) {
511    return false;
512  }
513
514  const currentPosted = normalizePostedValue(routeState && routeState.posted);
515  const baselinePosted = normalizePostedValue(baseline.posted || '');
516  if (currentPosted !== baselinePosted) {
517    return false;
518  }
519
520  const currentOp = normalizeRouteStateOperator(routeState && routeState.skillsOperator);
521  const baselineOp = normalizeRouteStateOperator(baseline.skillsOperator);
522  if (currentOp !== baselineOp) {
523    return false;
524  }
525
526  return true;
527}
528
529function shouldKeepVanityPath(indexRouteState, canonicalTrim) {
530  if (!(window.onVanity && window.presetCanonicalPath)) {
531    return false;
532  }
533  if (canonicalTrim !== window.presetCanonicalPath) {
534    return false;
535  }
536  const vanityTrim = (window.vanityPath || '').replace(/\/+$/, '');
537  const isRootVanity = vanityTrim === '';
538  if (window.lastFullRouteStateForVanity && typeof window.lastFullRouteStateForVanity === 'object') {
539    const fullState = window.lastFullRouteStateForVanity;
540    const hasMeaningfulFilter = !!(
541      fullState.category ||
542      fullState.location ||
543      fullState.occupation ||
544      fullState.experience ||
545      fullState['employment-type'] ||
546      fullState.skills ||
547      fullState.lang ||
548      fullState.query ||
549      fullState.min_salary ||
550      fullState.posted
551    );
552    if (!hasMeaningfulFilter) {
553      return isRootVanity;
554    }
555    return isCurrentRouteStatePresetBaseline(window.lastFullRouteStateForVanity);
556  }
557  if (isRootVanity) {
558    return true;
559  }
560  if (typeof window.currentRouteMatchesPresetBaseline === 'boolean') {
561    return window.currentRouteMatchesPresetBaseline;
562  }
563  return isCurrentRouteStatePresetBaseline(indexRouteState);
564}
565
566const customRouter = {
567  read() {
568    return {
569      remotive_unlimited: parseRouteStateFromURL(),
570    };
571  },
572
573  write(routeState) {
574    const indexRouteState = routeState.remotive_unlimited || {};
575    const canonicalPath = buildPathFromRouteState();
576    const trimmedRouteState = trimRouteStateAgainstBaseline(indexRouteState);
577    const searchStr = buildSearchFromRouteState(indexRouteState);
578    const vanitySearchStr = buildSearchFromRouteState(trimmedRouteState);
579    const canonicalTrim = canonicalPath.replace(/\/+$/, '');
580
581    if (!window.onVanity) {
582      const isEmptyState = Object.keys(indexRouteState).length === 0;
583      if (isEmptyState) {
584        const currentTrim = window.location.pathname.replace(/\/+$/, '');
585        if (currentTrim !== '' || (window.location.search || '') !== '') {
586          window.history.pushState({}, '', '/' + (searchStr || ''));
587        }
588        return;
589      }
590    }
591
592    if (window.onVanity && window.presetCanonicalPath) {
593      const currentTrim = window.location.pathname.replace(/\/+$/, '');
594      const currentSearch = window.location.search || '';
595      const vanityTrim = (window.vanityPath || '').replace(/\/+$/, '');
596      const keepVanityPath = shouldKeepVanityPath(indexRouteState, canonicalTrim);
597      if (keepVanityPath) {
598        if ((currentTrim !== vanityTrim && vanityTrim) || currentSearch !== (vanitySearchStr || '')) {
599          window.history.pushState({}, '', (vanityTrim || '/') + (vanitySearchStr || ''));
600        }
601        return;
602      } else {
603        if (currentTrim !== canonicalTrim || currentSearch !== (searchStr || '')) {
604          window.history.pushState({}, '', (canonicalPath || (ROUTE_BASE_PATH || '/')) + (searchStr || ''));
605        }
606        return;
607      }
608    }
609
610    const currentPath = window.location.pathname.replace(/\/+$/, '');
611    const currentSearch = window.location.search || '';
612    const nextPath = canonicalTrim;
613    if (currentPath !== nextPath || currentSearch !== (searchStr || '')) {
614      window.history.pushState({}, '', (canonicalPath || (ROUTE_BASE_PATH || '/')) + (searchStr || ''));
615    }
616  },
617
618  createURL(routeState) {
619    const indexRouteState = routeState.remotive_unlimited || {};
620    const canonicalPath = buildPathFromRouteState();
621    const trimmedRouteState = trimRouteStateAgainstBaseline(indexRouteState);
622    const searchStr = buildSearchFromRouteState(indexRouteState);
623    const vanitySearchStr = buildSearchFromRouteState(trimmedRouteState);
624    const canonicalTrim = canonicalPath.replace(/\/+$/, '');
625
626    if (!window.onVanity) {
627      const isEmptyState = Object.keys(indexRouteState).length === 0;
628      if (isEmptyState) {
629        return '/' + (searchStr || '');
630      }
631    }
632
633    if (window.onVanity && window.presetCanonicalPath) {
634      const vanityPath = window.vanityPath || (ROUTE_BASE_PATH || '/');
635      const keepVanityPath = shouldKeepVanityPath(indexRouteState, canonicalTrim);
636      if (keepVanityPath) {
637        return (vanityPath || '/') + (vanitySearchStr || '');
638      } else {
639        return (canonicalPath || (ROUTE_BASE_PATH || '/')) + (searchStr || '');
640      }
641    }
642
643    return (canonicalPath || (ROUTE_BASE_PATH || '/')) + (searchStr || '');
644  },
645
646  onUpdate(callback) {
647    window.addEventListener('popstate', callback);
648  },
649};
650
651const routing = {
652  router: customRouter,
653  stateMapping: {
654    stateToRoute(uiState) {
655      return {
656        remotive_unlimited: stateToRoute(uiState),
657      };
658    },
659    routeToState(routeState) {
660      return routeToState();
661    },
662  },
663};
664

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.