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.ts143
1 files changed, 143 insertions, 0 deletions
diff --git a/viewer/src/services/indexfactory.ts b/viewer/src/services/indexfactory.ts
new file mode 100644
index 0000000..e402185
--- /dev/null
+++ b/viewer/src/services/indexfactory.ts
@@ -0,0 +1,143 @@
1/* ldgallery - A static generator which turns a collection of tagged
2-- pictures into a searchable web gallery.
3--
4-- Copyright (C) 2019-2020 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 { Operation } from "@/@types/Operation";
21import Navigation from "@/services/navigation";
22
23export default class IndexFactory {
24
25 public static generateTags(root: Gallery.Item | null): Tag.Index {
26 let tagsIndex: Tag.Index = {};
27 if (root) IndexFactory.pushTagsForItem(tagsIndex, root);
28 return tagsIndex;
29 }
30
31 // Pushes all tags for a root item (and its children) to the index
32 private static pushTagsForItem(tagsIndex: Tag.Index, item: Gallery.Item): void {
33 if (item.properties.type === "directory") {
34 item.properties.items.forEach(item => this.pushTagsForItem(tagsIndex, item));
35 return; // Directories are not indexed
36 }
37 for (const tag of item.tags) {
38 const parts = tag.split(":");
39 let lastPart: string | null = null;
40 for (const part of parts) {
41 tagsIndex[part] = IndexFactory.pushPartToIndex(tagsIndex[part], part, item, !Boolean(lastPart));
42 if (lastPart) {
43 const children = tagsIndex[lastPart].children;
44 children[part] = IndexFactory.pushPartToIndex(children[part], part, item, false);
45 }
46 lastPart = part;
47 }
48 if (lastPart) tagsIndex[lastPart].childPart = true;
49 }
50 }
51
52 private static pushPartToIndex(index: Tag.Node, part: string, item: Gallery.Item, rootPart: boolean): Tag.Node {
53 if (!index) index = { tag: part, tagfiltered: Navigation.normalize(part), rootPart, childPart: !rootPart, items: [], children: {} };
54 else if (rootPart) index.rootPart = true;
55 else index.childPart = true;
56
57 if (!index.items.includes(item)) index.items.push(item);
58 return index;
59 }
60
61 // ---
62
63
64 public static searchTags(tagsIndex: Tag.Index, filter: string, strict: boolean): Tag.Search[] {
65 let search: Tag.Search[] = [];
66 if (tagsIndex && filter) {
67 const operation = IndexFactory.extractOperation(filter);
68 if (operation !== Operation.INTERSECTION) filter = filter.slice(1);
69 if (filter.includes(":")) {
70 const filterParts = filter.split(":");
71 search = this.searchTagsFromFilterWithCategory(tagsIndex, operation, filterParts[0], filterParts[1], strict);
72 } else {
73 search = this.searchTagsFromFilter(tagsIndex, operation, filter, strict);
74 }
75 }
76 return search;
77 }
78
79 private static extractOperation(filter: string): Operation {
80 const first = filter.slice(0, 1);
81 switch (first) {
82 case Operation.ADDITION:
83 case Operation.SUBSTRACTION:
84 return first;
85 default:
86 return Operation.INTERSECTION;
87 }
88 }
89
90 private static searchTagsFromFilterWithCategory(
91 tagsIndex: Tag.Index,
92 operation: Operation,
93 category: string,
94 disambiguation: string,
95 strict: boolean
96 ): Tag.Search[] {
97 category = Navigation.normalize(category);
98 disambiguation = Navigation.normalize(disambiguation);
99 return Object.values(tagsIndex)
100 .filter(node => IndexFactory.matches(node, category, strict))
101 .flatMap(node =>
102 Object.values(node.children)
103 .filter(child => IndexFactory.matches(child, disambiguation, strict))
104 .map(child => ({ ...child, parent: node, operation, display: `${operation}${node.tag}:${child.tag}` }))
105 );
106 }
107
108 private static searchTagsFromFilter(tagsIndex: Tag.Index, operation: Operation, filter: string, strict: boolean): Tag.Search[] {
109 filter = Navigation.normalize(filter);
110 return Object.values(tagsIndex)
111 .filter(node => IndexFactory.matches(node, filter, strict))
112 .map(node => ({ ...node, operation, display: `${operation}${node.tag}` }));
113 }
114
115 private static matches(node: Tag.Node, filter: string, strict: boolean): boolean {
116 if (strict) return node.tagfiltered === filter;
117 return node.tagfiltered.includes(filter)
118 }
119
120 // ---
121
122 public static generateCategories(tagsIndex: Tag.Index, categoryTags?: Gallery.RawTag[]): Tag.Category[] {
123 if (!categoryTags?.length) return [{ tag: "", index: tagsIndex }];
124
125 const tagsCategories: Tag.Category[] = [];
126 const tagsRemaining = new Map(Object.entries(tagsIndex));
127 categoryTags
128 .map(tag => ({ tag, index: tagsIndex[tag]?.children }))
129 .filter(category => category.index && Object.keys(category.index).length)
130 .forEach(category => {
131 tagsCategories.push(category);
132 [category.tag, ...Object.values(category.index).map(node => node.tag)]
133 .filter(tag => IndexFactory.isDiscriminantTagOnly(categoryTags, tagsIndex[tag]))
134 .forEach(tag => tagsRemaining.delete(tag));
135 });
136 tagsCategories.push({ tag: "", index: Object.fromEntries(tagsRemaining) });
137 return tagsCategories;
138 }
139
140 private static isDiscriminantTagOnly(tags: Gallery.RawTag[], node: Tag.Node): boolean {
141 return !tags.includes(node.tag) || !node.childPart;
142 }
143}