aboutsummaryrefslogtreecommitdiff
path: root/src/client/util/SearchUtil.ts
blob: 2f23d07dcd1469e75199c9322a1f593f89eacedf (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
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<Doc>, 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<Doc, string[]>();
        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<string>();
                    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 Object.keys(Doc.GetAllPrototypes(doc).filter(proto => proto).reduce(
            (keys, proto) => {
                Object.keys(proto).forEach(keys.add.bind(keys));
                return keys;
            },
            new Set<string>())); // 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++;
        }
    }
}