aboutsummaryrefslogtreecommitdiff
path: root/viewer/src/services/indexFactory.ts
diff options
context:
space:
mode:
Diffstat (limited to 'viewer/src/services/indexFactory.ts')
-rw-r--r--viewer/src/services/indexFactory.ts163
1 files changed, 163 insertions, 0 deletions
diff --git a/viewer/src/services/indexFactory.ts b/viewer/src/services/indexFactory.ts
new file mode 100644
index 0000000..a414856
--- /dev/null
+++ b/viewer/src/services/indexFactory.ts
@@ -0,0 +1,163 @@
1/* ldgallery - A static generator which turns a collection of tagged
2-- pictures into a searchable web gallery.
3--
4-- Copyright (C) 2019-2022 Guillaume FOUET
5--
6-- This program is free software: you can redistribute it and/or modify
7-- it under the terms of the GNU Affero General Public License as
8-- published by the Free Software Foundation, either version 3 of the
9-- License, or (at your option) any later version.
10--
11-- This program is distributed in the hope that it will be useful,
12-- but WITHOUT ANY WARRANTY; without even the implied warranty of
13-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14-- GNU Affero General Public License for more details.
15--
16-- You should have received a copy of the GNU Affero General Public License
17-- along with this program. If not, see <https://www.gnu.org/licenses/>.
18*/
19
20import { Item, RawTag } from '@/@types/gallery';
21import { Operation } from '@/@types/operation';
22import { TagCategory, TagIndex, TagNode, TagSearch } from '@/@types/tag';
23import { isDirectory } from './itemGuards';
24import { useNavigation } from './navigation';
25
26const navigation = useNavigation();
27
28function _pushPartToIndex(index: TagNode, part: string, item: Item, rootPart: boolean): TagNode {
29 if (!index) {
30 index = {
31 tag: part,
32 tagfiltered: navigation.normalize(part),
33 rootPart,
34 childPart: !rootPart,
35 items: [],
36 children: {},
37 };
38 } else if (rootPart) index.rootPart = true;
39 else index.childPart = true;
40
41 if (!index.items.includes(item)) index.items.push(item);
42 return index;
43}
44
45// Pushes all tags for a root item (and its children) to the index
46function _pushTagsForItem(tagsIndex: TagIndex, item: Item): void {
47 if (isDirectory(item)) {
48 item.properties.items.forEach(item => _pushTagsForItem(tagsIndex, item));
49 return; // Directories are not indexed
50 }
51 for (const tag of item.tags) {
52 const parts = tag.split(':');
53 let lastPart: string | null = null;
54 for (const part of parts) {
55 tagsIndex[part] = _pushPartToIndex(tagsIndex[part], part, item, !lastPart);
56 if (lastPart) {
57 const children = tagsIndex[lastPart].children;
58 children[part] = _pushPartToIndex(children[part], part, item, false);
59 }
60 lastPart = part;
61 }
62 if (lastPart) tagsIndex[lastPart].childPart = true;
63 }
64}
65
66function _extractOperation(filter: string): Operation {
67 const first = filter.slice(0, 1);
68 switch (first) {
69 case Operation.ADDITION:
70 case Operation.SUBSTRACTION:
71 return first;
72 default:
73 return Operation.INTERSECTION;
74 }
75}
76
77function _searchTagsFromFilterWithCategory(
78 tagsIndex: TagIndex,
79 operation: Operation,
80 category: string,
81 disambiguation: string,
82 strict: boolean,
83): TagSearch[] {
84 category = navigation.normalize(category);
85 disambiguation = navigation.normalize(disambiguation);
86 return Object.values(tagsIndex)
87 .filter(node => _matches(node, category, strict))
88 .flatMap(node =>
89 Object.values(node.children)
90 .filter(child => _matches(child, disambiguation, strict))
91 .map(child => ({ ...child, parent: node, operation, display: `${operation}${node.tag}:${child.tag}` })),
92 );
93}
94
95function _searchTagsFromFilter(
96 tagsIndex: TagIndex,
97 operation: Operation,
98 filter: string,
99 strict: boolean,
100): TagSearch[] {
101 filter = navigation.normalize(filter);
102 return Object.values(tagsIndex)
103 .filter(node => _matches(node, filter, strict))
104 .map(node => ({ ...node, operation, display: `${operation}${node.tag}` }));
105}
106
107function _matches(node: TagNode, filter: string, strict: boolean): boolean {
108 if (strict) return node.tagfiltered === filter;
109 return node.tagfiltered.includes(filter);
110}
111
112function _isDiscriminantTagOnly(tags: RawTag[], node: TagNode): boolean {
113 return !tags.includes(node.tag) || !node.childPart;
114}
115
116// ---
117
118export const useIndexFactory = () => {
119 function generateTags(root: Item | null): TagIndex {
120 const tagsIndex: TagIndex = {};
121 if (root) _pushTagsForItem(tagsIndex, root);
122 return tagsIndex;
123 }
124
125 function searchTags(tagsIndex: TagIndex, filter: string, strict: boolean): TagSearch[] {
126 let search: TagSearch[] = [];
127 if (tagsIndex && filter) {
128 const operation = _extractOperation(filter);
129 if (operation !== Operation.INTERSECTION) filter = filter.slice(1);
130 if (filter.includes(':')) {
131 const filterParts = filter.split(':');
132 search = _searchTagsFromFilterWithCategory(tagsIndex, operation, filterParts[0], filterParts[1], strict);
133 } else {
134 search = _searchTagsFromFilter(tagsIndex, operation, filter, strict);
135 }
136 }
137 return search;
138 }
139
140 function generateCategories(tagsIndex: TagIndex, categoryTags?: RawTag[]): TagCategory[] {
141 if (!categoryTags?.length) return [{ tag: '', index: tagsIndex }];
142
143 const tagsCategories: TagCategory[] = [];
144 const tagsRemaining = new Map(Object.entries(tagsIndex));
145 categoryTags
146 .map(tag => ({ tag, index: tagsIndex[tag]?.children }))
147 .filter(category => category.index && Object.keys(category.index).length)
148 .forEach(category => {
149 tagsCategories.push(category);
150 [category.tag, ...Object.values(category.index).map(node => node.tag)]
151 .filter(tag => _isDiscriminantTagOnly(categoryTags, tagsIndex[tag]))
152 .forEach(tag => tagsRemaining.delete(tag));
153 });
154 tagsCategories.push({ tag: '', index: Object.fromEntries(tagsRemaining) });
155 return tagsCategories;
156 }
157
158 return {
159 generateTags,
160 searchTags,
161 generateCategories,
162 };
163};