-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathutils.js
More file actions
154 lines (132 loc) · 4.43 KB
/
Copy pathutils.js
File metadata and controls
154 lines (132 loc) · 4.43 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
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
/**
* Shared utility functions for Splatoon 3 Rotation Tracker
*/
// API endpoints
const API = {
SCHEDULES: 'https://splatoon3.ink/data/schedules.json',
REFRESH_INTERVAL: 30 // minutes
};
/**
* Normalize a stage name to a filesystem-safe ID
* This automatically handles most new stages without manual mapping
* @param {string} name The stage name to normalize
* @returns {string} Normalized stage ID
*/
function normalizeStageId(name) {
if (typeof name !== 'string') return 'unknown_stage';
const normalized = name
.toLowerCase()
.trim()
.replace(/['\u2018\u2019]/g, '')
.replace(/&/g, 'and')
.replace(/[^a-z0-9]+/g, '_')
.replace(/^_+|_+$/g, '');
return normalized || 'unknown_stage';
}
// Exceptions where normalizeStageId() doesn't produce the expected image filename.
// Keyed on the *normalized* form (output of normalizeStageId), so any input
// the normalizer maps to the same value gets caught here.
const stageIdOverrides = {
// "Um'ami Ruins": apostrophe removal yields "umami_ruins"; the image keeps the split.
"umami_ruins": "um_ami_ruins",
// "Salmonid Smokeyard": the image filename is truncated ("smokeyar").
"salmonid_smokeyard": "salmonid_smokeyar",
// "Mako Mart" / "MakoMart": the API spaces it; the image file collapses it.
"mako_mart": "makomart",
// "Splatlands Bowl" is the short form of Grand Splatlands Bowl.
"splatlands_bowl": "grand_splatlands_bowl"
};
/**
* Format a date to a human-readable time
* @param {Date|string} date The date to format
* @returns {string} Formatted time string
*/
function formatTime(date) {
if (!date) return 'Unknown';
const dateObj = typeof date === 'string' ? new Date(date) : date;
if (isNaN(dateObj.getTime())) return 'Invalid Date';
const options = {
hour: 'numeric',
minute: '2-digit',
hour12: true
};
const timeString = dateObj.toLocaleTimeString(undefined, options);
// Add date if it's not today
const now = new Date();
const isToday = now.toDateString() === dateObj.toDateString();
if (isToday) {
return timeString;
} else {
// Add date
const dateOptions = {
weekday: 'short',
month: 'short',
day: 'numeric'
};
return `${dateObj.toLocaleDateString(undefined, dateOptions)} ${timeString}`;
}
}
/**
* Format timespan between start and end
* @param {string|Date} startTime Start time
* @param {string|Date} endTime End time
* @returns {string} Formatted timespan
*/
function formatTimeRange(startTime, endTime) {
const start = typeof startTime === 'string' ? new Date(startTime) : startTime;
const end = typeof endTime === 'string' ? new Date(endTime) : endTime;
return `${formatTime(start)} - ${formatTime(end)}`;
}
/**
* Get stage image ID from stage name. Normalizes via normalizeStageId, then
* applies any override from stageIdOverrides for cases where the image
* filename doesn't match the normalized form.
* @param {string} stageName The stage name to convert
* @returns {string} The stage ID for use in image paths
*/
function getStageId(stageName) {
if (!stageName) {
console.warn('getStageId called with empty/null stage name');
return 'unknown_stage';
}
const normalizedId = normalizeStageId(stageName);
return Object.hasOwn(stageIdOverrides, normalizedId)
? stageIdOverrides[normalizedId]
: normalizedId;
}
/**
* Accept an HTTPS image URL hosted by splatoon3.ink or one of its subdomains.
* @param {unknown} value Candidate URL
* @returns {string} Normalized safe URL, or an empty string
*/
function safeSplatoonUrl(value) {
if (typeof value !== 'string' || !value) return '';
try {
const parsed = new URL(value);
const isAllowedHost = parsed.hostname === 'splatoon3.ink' ||
parsed.hostname.endsWith('.splatoon3.ink');
const hasCredentials = Boolean(parsed.username || parsed.password);
if (parsed.protocol === 'https:' && isAllowedHost && !hasCredentials && !parsed.port) {
return parsed.toString();
}
} catch {
// Invalid URLs are rejected below.
}
return '';
}
// Export utilities
const Utils = {
API,
stageIdOverrides,
normalizeStageId,
formatTime,
formatTimeRange,
getStageId,
safeSplatoonUrl
};
// Make utils available in different contexts
if (typeof window !== 'undefined') {
window.Utils = Utils;
} else if (typeof self !== 'undefined') {
self.Utils = Utils;
}