-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils.ts
More file actions
executable file
·103 lines (86 loc) · 3.38 KB
/
Copy pathutils.ts
File metadata and controls
executable file
·103 lines (86 loc) · 3.38 KB
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
100
101
102
export function withoutHash(tag: string): string {
return tag.startsWith("#") ? tag.slice(1) : tag;
}
export function normalize(tag: string): string {
// normalize to lowercase; keep slashes for nesting
return withoutHash(tag).trim().replace(/\s+/g, "-").toLowerCase();
}
export function normalizeTag(input: string): string {
if (!input) return "";
let s = String(input).trim();
s = s.replace(/^#+/, ""); // drop leading #s
s = s.replace(/\s+/g, "-"); // spaces -> dash (matches your prior normalize)
s = s.replace(/\\+/g, "/"); // backslashes -> forward slash (safety)
s = s.replace(/\/+/g, "/"); // collapse // -> /
s = s.replace(/^\/|\/$/g, ""); // trim edge slashes
return s.toLowerCase();
}
export function splitTagString(input: string): string[] {
return String(input)
.split(/[\s,]+/) // split on spaces or commas
.map(normalizeTag)
.filter(Boolean);
}
export function intersectSets<T>(a: Set<T>, b: Set<T>): Set<T> {
if (a.size > b.size) return intersectSets(b, a);
const out = new Set<T>();
for (const v of a) if (b.has(v)) out.add(v);
return out;
}
export function unionAll<T>(sets: Iterable<Set<T>>): Set<T> {
const out = new Set<T>();
for (const s of sets) for (const v of s) out.add(v);
return out;
}
export function sortedByCountDesc(map: Map<string, number>): [string, number][] {
return Array.from(map.entries()).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
}
export function firstSegment(tag: string): string {
const i = tag.indexOf("/");
return i === -1 ? tag : tag.slice(0, i);
}
// Tag tree utilities for hierarchical co-tag display
export interface TagTreeNode {
label: string; // segment label, e.g. "cyberdefenders"
full: string; // full path up to this node, e.g. "category/ctf/cyberdefenders"
count: number; // rolled-up count (sum of all descendants)
exactCount: number; // count for this exact tag (0 if none)
children: Map<string, TagTreeNode>;
expanded?: boolean; // whether this node is expanded (optional)
}
// Build a hierarchical tree from a frequency map of tags.
// freq: Map<"a/b/c", count>. Parents are created and get rolled-up counts.
export function buildTagTree(
freq: Map<string, number>,
delimiter = "/"
): Map<string, TagTreeNode> {
const roots = new Map<string, TagTreeNode>();
for (const [tag, n] of freq) {
const parts = tag.split(delimiter).filter(Boolean);
if (!parts.length) continue;
let acc = "";
let layer = roots;
for (let i = 0; i < parts.length; i++) {
acc = i === 0 ? parts[0] : `${acc}${delimiter}${parts[i]}`;
const seg = parts[i];
let node = layer.get(seg);
if (!node) {
node = { label: seg, full: acc, count: 0, exactCount: 0, children: new Map() };
layer.set(seg, node);
}
node.count += n; // roll-up
if (i === parts.length - 1) node.exactCount += n; // leaf contributes exact count
layer = node.children;
}
}
return roots;
}
// Optional: sort helpers for stable rendering
export function sortNodes(
nodes: Iterable<TagTreeNode>,
mode: "count" | "alpha" = "count"
): TagTreeNode[] {
const arr = Array.from(nodes);
if (mode === "alpha") return arr.sort((a, b) => a.label.localeCompare(b.label));
return arr.sort((a, b) => b.count - a.count || a.label.localeCompare(b.label));
}