|
(function() { |
|
/** |
|
* Scans through an object to find a specified term somewhere within the structure |
|
* @param {string|function} term - The term to search for. |
|
* If it's a function, will be used to determine matches manually - takes a string as input, and should return a boolean. |
|
* Otherwise, will coerce to a lowercased string for checks. |
|
* @param {object} [options] The options for scanning |
|
* @param {number} [options.maxIterations=65536] - The maximum number of objects to scan before terminating (to prevent endless loops) |
|
* @param {number} [options.maxDepth=8] - The maximum depth of recursion to go to for any given path |
|
* @param {boolean} [options.verbose=false] - Whether or not to provide verbose debug logging |
|
* @param {object} [options.root=window] - The root object to scan. Defaults to window. |
|
* @returns {object[]} Search results, containing the value, object, and level for each find |
|
*/ |
|
function searchForTerm(term, options = {}) { |
|
options = { |
|
maxIterations: 65536, |
|
maxDepth: 8, |
|
verbose: false, |
|
root: window, |
|
...options |
|
}; |
|
|
|
const state = { |
|
results: [], |
|
i: 0, |
|
options |
|
}; |
|
|
|
let func; |
|
if (typeof term === 'function') { |
|
func = term; |
|
} else { |
|
term = term.toString().toLowerCase(); |
|
func = function(k) { |
|
return k.toString().toLowerCase().includes(term); |
|
} |
|
} |
|
|
|
function scan(obj, level) { |
|
state.i++; |
|
|
|
if (level.length >= options.maxDepth) { |
|
if (options.verbose) { |
|
console.log('Exceeded maximum depth (%i/%i), breaking...', level.length, options.maxDepth); |
|
} |
|
return; |
|
} |
|
|
|
if (options.verbose) { |
|
console.log('[%i] Scanning: %s', state.i, level.join('.')) |
|
} |
|
|
|
for (const key of Object.keys(obj)) { |
|
const val = obj[key]; |
|
const l = [...level, key]; |
|
|
|
if (obj === val || val === options.root) { |
|
continue; |
|
} |
|
|
|
let o = options.root; |
|
let cont = true; |
|
for (const lev of level) { |
|
o = o[lev]; |
|
if (o === val) { |
|
if (options.verbose) { |
|
console.log('Skipping %s, duplicate object', l.join('.')); |
|
} |
|
cont = false; |
|
break; |
|
} |
|
} |
|
|
|
if (!cont) { |
|
continue; |
|
} |
|
|
|
if (state.i >= state.options.maxIterations) { |
|
console.log('Exceeded maximum iterations (%i/%i), terminating...', state.i, options.maxIterations); |
|
return false; |
|
} |
|
|
|
if (!val) continue; |
|
|
|
let res; |
|
if (typeof val === 'object') { |
|
res = scan(val, l); |
|
} else if (val && func(val)) { |
|
state.results.push({ |
|
obj, level: l.join('.'), val |
|
}); |
|
} |
|
|
|
if (res === false) { |
|
return false; |
|
} |
|
} |
|
} |
|
|
|
scan(options.root, []); |
|
|
|
return state.results; |
|
} |
|
|
|
const output = [ |
|
'===[ Object Scanner ]===', |
|
'See https://gist.github.com/Ratismal/8f2659bf7b0cf50a8fc9b8d8343f2b84 for usage instructions.', |
|
'', |
|
'Examples:', |
|
'searchForTerm(\'term\');', |
|
'searchForTerm(function(value) {', |
|
' return value === \'term\';', |
|
'});', |
|
'searchForTerm(\'term\', {', |
|
' verbose: true,', |
|
'});', |
|
]; |
|
|
|
console.log(output.join('\n')); |
|
|
|
try { |
|
window.searchForTerm = searchForTerm; |
|
} catch (err) { |
|
try { |
|
global.searchForTerm = searchForTerm; |
|
} catch (err) { |
|
console.error('Could not append searchForTerm to a global namespace (tried `window` and `global`)'); |
|
} |
|
} |
|
})(); |