import { ObservableMap } from 'mobx'; import { Doc, DocListCast, Field, FieldType, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { StrCast } from '../../fields/Types'; import { DocumentType } from '../documents/DocumentTypes'; import { DocOptions, FInfo } from '../documents/Documents'; export namespace SearchUtil { export type HighlightingResult = { [id: string]: { [key: string]: string[] } }; /** * Recursively search all Docs within the collection for the query string. * @param {Doc} collectionDoc - The collection document to search within. * @param {string} queryIn - The query string to search for. * @param {boolean} matchKeyNames - Whether to match metadata field names in addition to field values * @param {string[]} onlyKeys - Optional: restrict search to only look in the specified array of field names. */ export function SearchCollection(collectionDoc: Opt, queryIn: string, matchKeyNames: boolean, onlyKeys?: string[]) { const blockedTypes = [DocumentType.PRESSLIDE, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; const blockedKeys = matchKeyNames ? [] : Object.entries(DocOptions) .filter(([, info]: [string, FieldType | FInfo | undefined]) => (info instanceof FInfo ? !info.searchable() : true)) .map(([key]) => key); const exact = queryIn.startsWith('='); const query = queryIn.toLowerCase().split('=').lastElement(); const results = new ObservableMap(); if (collectionDoc) { const docs = DocListCast(collectionDoc[Doc.LayoutDataKey(collectionDoc)]); const docIDs: string[] = []; SearchUtil.foreachRecursiveDoc(docs, (depth: number, doc: Doc) => { const dtype = StrCast(doc.type) as DocumentType; if (dtype && !blockedTypes.includes(dtype) && !docIDs.includes(doc[Id]) && depth >= 0) { const hlights = new Set(); const fieldsToSearch = onlyKeys ?? SearchUtil.documentKeys(doc); fieldsToSearch.forEach( key => { const val = (matchKeyNames ? key : Field.toString(doc[key] as FieldType)).toLowerCase(); const accept = (exact ? val === query.toLowerCase() : val.includes(query.toLowerCase())); accept && hlights.add(key); } ); // prettier-ignore blockedKeys.forEach(key => hlights.delete(key)); if (Array.from(hlights.keys()).length > 0) { results.set(doc, Array.from(hlights.keys())); } } docIDs.push(doc[Id]); }); } return results; } /** * @param {Doc} doc - Doc to search for used field names * * An array of all field names used by the Doc or its prototypes. */ export function documentKeys(doc: Doc) { return Array.from(Doc.GetAllPrototypes(doc).filter(proto => proto).reduce( (keys, proto) => { Object.keys(proto).forEach(keys.add.bind(keys)); return keys; }, new Set())); // prettier-ignore } /** * @param {Doc[]} docs - docs to be searched through recursively * @param {number, Doc => void} func - function to be called on each doc * * This method iterates through an array of docs and all docs within those docs, calling * the function func on each doc. */ export function foreachRecursiveDoc(docsIn: Doc[], func: (depth: number, doc: Doc) => void) { let docs = docsIn; let newarray: Doc[] = []; let depth = 0; const visited: Doc[] = []; while (docs.length > 0) { newarray = []; // eslint-disable-next-line no-loop-func docs.filter(d => d && !visited.includes(d)).forEach(d => { visited.push(d); const fieldKey = Doc.LayoutDataKey(d); const annos = !Field.toString(Doc.LayoutField(d) as FieldType).includes('CollectionView'); const data = d[annos ? fieldKey + '_annotations' : fieldKey]; data && newarray.push(...DocListCast(data)); const sidebar = d[fieldKey + '_sidebar']; sidebar && newarray.push(...DocListCast(sidebar)); func(depth, d); }); docs = newarray; depth++; } } }