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 Array.from(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++;
}
}
}
|