@yuiseki/gyazocli 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +66 -0
- package/dist/commands/apps.js +72 -0
- package/dist/commands/collection.js +45 -0
- package/dist/commands/config.js +64 -0
- package/dist/commands/domains.js +72 -0
- package/dist/commands/get.js +70 -0
- package/dist/commands/import.js +74 -0
- package/dist/commands/list.js +101 -0
- package/dist/commands/locations.js +72 -0
- package/dist/commands/search.js +43 -0
- package/dist/commands/stats.js +81 -0
- package/dist/commands/summary.js +42 -0
- package/dist/commands/sync.js +95 -0
- package/dist/commands/tags.js +70 -0
- package/dist/commands/upload.js +72 -0
- package/dist/dates.js +250 -0
- package/dist/format.js +375 -0
- package/dist/ids.js +77 -0
- package/dist/index.js +55 -2454
- package/dist/mcp.js +371 -0
- package/dist/options.js +20 -0
- package/dist/services/analytics.js +474 -0
- package/dist/services/collections.js +97 -0
- package/dist/services/images.js +184 -0
- package/dist/services/memory.js +373 -0
- package/docs/ADR/003-cli-structure.md +3 -1
- package/docs/ADR/004-mcp-server.md +94 -0
- package/docs/ADR/005-module-layout.md +72 -0
- package/package.json +2 -1
package/dist/format.js
ADDED
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Reading and presenting the fields of a Gyazo image: the domain a capture
|
|
4
|
+
* came from, the address recorded in its EXIF, its OCR text, the objects
|
|
5
|
+
* detected in it. Nothing here talks to the API or the cache, so both the CLI
|
|
6
|
+
* and the aggregations can share it.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.normalizeText = normalizeText;
|
|
10
|
+
exports.extractDomain = extractDomain;
|
|
11
|
+
exports.isXDomain = isXDomain;
|
|
12
|
+
exports.cleanTextForDomain = cleanTextForDomain;
|
|
13
|
+
exports.stripInlineUrls = stripInlineUrls;
|
|
14
|
+
exports.sanitizeSummaryText = sanitizeSummaryText;
|
|
15
|
+
exports.getAddressEntry = getAddressEntry;
|
|
16
|
+
exports.getAddressComponent = getAddressComponent;
|
|
17
|
+
exports.buildJaLocationLabel = buildJaLocationLabel;
|
|
18
|
+
exports.buildEnLocationLabel = buildEnLocationLabel;
|
|
19
|
+
exports.extractImageAddressText = extractImageAddressText;
|
|
20
|
+
exports.extractImageLocationLabel = extractImageLocationLabel;
|
|
21
|
+
exports.truncateText = truncateText;
|
|
22
|
+
exports.formatCreatedAt = formatCreatedAt;
|
|
23
|
+
exports.shortenImageId = shortenImageId;
|
|
24
|
+
exports.formatTerminalLink = formatTerminalLink;
|
|
25
|
+
exports.normalizeOcrText = normalizeOcrText;
|
|
26
|
+
exports.extractOcrDescription = extractOcrDescription;
|
|
27
|
+
exports.buildOcrPreview = buildOcrPreview;
|
|
28
|
+
exports.extractObjectAnnotations = extractObjectAnnotations;
|
|
29
|
+
exports.formatObjectAnnotationLine = formatObjectAnnotationLine;
|
|
30
|
+
exports.extractImageApps = extractImageApps;
|
|
31
|
+
exports.extractImageDomains = extractImageDomains;
|
|
32
|
+
exports.extractImageLocations = extractImageLocations;
|
|
33
|
+
exports.normalizeTagText = normalizeTagText;
|
|
34
|
+
exports.extractTagFromLinkValue = extractTagFromLinkValue;
|
|
35
|
+
exports.extractImageTags = extractImageTags;
|
|
36
|
+
exports.normalizeRankingValues = normalizeRankingValues;
|
|
37
|
+
exports.shouldEnrichForLocationDisplay = shouldEnrichForLocationDisplay;
|
|
38
|
+
exports.mergeImageForDisplay = mergeImageForDisplay;
|
|
39
|
+
function normalizeText(value) {
|
|
40
|
+
if (!value)
|
|
41
|
+
return undefined;
|
|
42
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
43
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
44
|
+
}
|
|
45
|
+
function extractDomain(value) {
|
|
46
|
+
if (!value)
|
|
47
|
+
return undefined;
|
|
48
|
+
try {
|
|
49
|
+
const url = new URL(value);
|
|
50
|
+
return url.hostname.replace(/^www\./, '');
|
|
51
|
+
}
|
|
52
|
+
catch (e) {
|
|
53
|
+
try {
|
|
54
|
+
const url = new URL(`https://${value}`);
|
|
55
|
+
return url.hostname.replace(/^www\./, '');
|
|
56
|
+
}
|
|
57
|
+
catch (_e) {
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function isXDomain(domain) {
|
|
63
|
+
if (!domain)
|
|
64
|
+
return false;
|
|
65
|
+
return domain === 'x.com' ||
|
|
66
|
+
domain.endsWith('.x.com') ||
|
|
67
|
+
domain === 'twitter.com' ||
|
|
68
|
+
domain.endsWith('.twitter.com');
|
|
69
|
+
}
|
|
70
|
+
function cleanTextForDomain(value, domain) {
|
|
71
|
+
if (!isXDomain(domain))
|
|
72
|
+
return value;
|
|
73
|
+
return value
|
|
74
|
+
.replace(/^Xユーザーの/, '')
|
|
75
|
+
.replace(/\s*\/\s*X$/, '')
|
|
76
|
+
.trim();
|
|
77
|
+
}
|
|
78
|
+
function stripInlineUrls(value) {
|
|
79
|
+
return value
|
|
80
|
+
.replace(/https?:\/\/\S+/g, '')
|
|
81
|
+
.replace(/\bwww\.\S+/g, '')
|
|
82
|
+
.replace(/\s+/g, ' ')
|
|
83
|
+
.trim();
|
|
84
|
+
}
|
|
85
|
+
function sanitizeSummaryText(value, domain) {
|
|
86
|
+
if (!value)
|
|
87
|
+
return undefined;
|
|
88
|
+
return normalizeText(stripInlineUrls(cleanTextForDomain(value, domain)));
|
|
89
|
+
}
|
|
90
|
+
function getAddressEntry(exifAddress, locale) {
|
|
91
|
+
if (!exifAddress || typeof exifAddress !== 'object')
|
|
92
|
+
return undefined;
|
|
93
|
+
if (typeof exifAddress.address === 'string')
|
|
94
|
+
return exifAddress;
|
|
95
|
+
const entry = exifAddress[locale];
|
|
96
|
+
if (!entry || typeof entry !== 'object')
|
|
97
|
+
return undefined;
|
|
98
|
+
return entry;
|
|
99
|
+
}
|
|
100
|
+
function getAddressComponent(addressEntry, type) {
|
|
101
|
+
if (!addressEntry || typeof addressEntry !== 'object')
|
|
102
|
+
return undefined;
|
|
103
|
+
const components = Array.isArray(addressEntry.address_components)
|
|
104
|
+
? addressEntry.address_components
|
|
105
|
+
: [];
|
|
106
|
+
for (const component of components) {
|
|
107
|
+
if (!component || typeof component !== 'object')
|
|
108
|
+
continue;
|
|
109
|
+
const types = Array.isArray(component.types) ? component.types : [];
|
|
110
|
+
if (!types.includes(type))
|
|
111
|
+
continue;
|
|
112
|
+
const value = normalizeText(component.long_name || component.short_name);
|
|
113
|
+
if (value)
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
return undefined;
|
|
117
|
+
}
|
|
118
|
+
function buildJaLocationLabel(exifAddress) {
|
|
119
|
+
const ja = getAddressEntry(exifAddress, 'ja');
|
|
120
|
+
if (!ja)
|
|
121
|
+
return undefined;
|
|
122
|
+
const pref = getAddressComponent(ja, 'administrative_area_level_1');
|
|
123
|
+
const locality = getAddressComponent(ja, 'locality') || getAddressComponent(ja, 'administrative_area_level_2');
|
|
124
|
+
const sublocality = getAddressComponent(ja, 'sublocality_level_2') ||
|
|
125
|
+
getAddressComponent(ja, 'sublocality_level_1') ||
|
|
126
|
+
getAddressComponent(ja, 'sublocality_level_3');
|
|
127
|
+
const fromComponents = normalizeText([pref, locality, sublocality].filter(Boolean).join(''));
|
|
128
|
+
if (fromComponents)
|
|
129
|
+
return fromComponents;
|
|
130
|
+
const raw = normalizeText(ja.address);
|
|
131
|
+
if (!raw)
|
|
132
|
+
return undefined;
|
|
133
|
+
const compact = raw
|
|
134
|
+
.replace(/^日本、?/, '')
|
|
135
|
+
.replace(/〒\d{3}-\d{4}\s*/g, '')
|
|
136
|
+
.replace(/[0-90-9].*$/, '')
|
|
137
|
+
.trim();
|
|
138
|
+
return normalizeText(compact);
|
|
139
|
+
}
|
|
140
|
+
function buildEnLocationLabel(exifAddress) {
|
|
141
|
+
const en = getAddressEntry(exifAddress, 'en');
|
|
142
|
+
if (!en)
|
|
143
|
+
return undefined;
|
|
144
|
+
const pref = getAddressComponent(en, 'administrative_area_level_1');
|
|
145
|
+
const locality = getAddressComponent(en, 'locality') || getAddressComponent(en, 'administrative_area_level_2');
|
|
146
|
+
const sublocality = getAddressComponent(en, 'sublocality_level_2') ||
|
|
147
|
+
getAddressComponent(en, 'sublocality_level_1') ||
|
|
148
|
+
getAddressComponent(en, 'sublocality_level_3');
|
|
149
|
+
const fromComponents = normalizeText([sublocality, locality, pref].filter(Boolean).join(', '));
|
|
150
|
+
if (fromComponents)
|
|
151
|
+
return fromComponents;
|
|
152
|
+
return normalizeText(en.address);
|
|
153
|
+
}
|
|
154
|
+
function extractImageAddressText(img) {
|
|
155
|
+
const exifAddress = img.metadata?.exif_address ?? img.exif_address;
|
|
156
|
+
if (!exifAddress)
|
|
157
|
+
return undefined;
|
|
158
|
+
if (typeof exifAddress === 'string')
|
|
159
|
+
return normalizeText(exifAddress);
|
|
160
|
+
if (typeof exifAddress !== 'object')
|
|
161
|
+
return undefined;
|
|
162
|
+
const ja = getAddressEntry(exifAddress, 'ja');
|
|
163
|
+
const jaAddress = normalizeText(ja?.address);
|
|
164
|
+
if (jaAddress)
|
|
165
|
+
return jaAddress;
|
|
166
|
+
const en = getAddressEntry(exifAddress, 'en');
|
|
167
|
+
const enAddress = normalizeText(en?.address);
|
|
168
|
+
if (enAddress)
|
|
169
|
+
return enAddress;
|
|
170
|
+
for (const value of Object.values(exifAddress)) {
|
|
171
|
+
if (!value || typeof value !== 'object')
|
|
172
|
+
continue;
|
|
173
|
+
const raw = normalizeText(value.address);
|
|
174
|
+
if (raw)
|
|
175
|
+
return raw;
|
|
176
|
+
}
|
|
177
|
+
return undefined;
|
|
178
|
+
}
|
|
179
|
+
function extractImageLocationLabel(img) {
|
|
180
|
+
const exifAddress = img.metadata?.exif_address ?? img.exif_address;
|
|
181
|
+
if (!exifAddress)
|
|
182
|
+
return undefined;
|
|
183
|
+
if (typeof exifAddress === 'string')
|
|
184
|
+
return normalizeText(exifAddress);
|
|
185
|
+
if (typeof exifAddress !== 'object')
|
|
186
|
+
return undefined;
|
|
187
|
+
const jaLabel = buildJaLocationLabel(exifAddress);
|
|
188
|
+
if (jaLabel)
|
|
189
|
+
return jaLabel;
|
|
190
|
+
const enLabel = buildEnLocationLabel(exifAddress);
|
|
191
|
+
if (enLabel)
|
|
192
|
+
return enLabel;
|
|
193
|
+
for (const value of Object.values(exifAddress)) {
|
|
194
|
+
if (!value || typeof value !== 'object')
|
|
195
|
+
continue;
|
|
196
|
+
const raw = normalizeText(value.address);
|
|
197
|
+
if (raw)
|
|
198
|
+
return raw;
|
|
199
|
+
}
|
|
200
|
+
return undefined;
|
|
201
|
+
}
|
|
202
|
+
function truncateText(value, maxLength) {
|
|
203
|
+
if (value.length <= maxLength)
|
|
204
|
+
return value;
|
|
205
|
+
if (maxLength <= 3)
|
|
206
|
+
return value.slice(0, maxLength);
|
|
207
|
+
return `${value.slice(0, maxLength - 3)}...`;
|
|
208
|
+
}
|
|
209
|
+
function formatCreatedAt(value) {
|
|
210
|
+
const match = value.match(/^(\d{4}-\d{2}-\d{2})[T ](\d{2}):(\d{2})/);
|
|
211
|
+
if (match) {
|
|
212
|
+
return `${match[1]} ${match[2]}:${match[3]}`;
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
function shortenImageId(imageId) {
|
|
217
|
+
if (!imageId)
|
|
218
|
+
return '';
|
|
219
|
+
if (imageId.length <= 4)
|
|
220
|
+
return imageId;
|
|
221
|
+
return `${imageId.slice(0, 4)}...`;
|
|
222
|
+
}
|
|
223
|
+
function formatTerminalLink(label, url) {
|
|
224
|
+
if (!url || !process.stdout.isTTY)
|
|
225
|
+
return label;
|
|
226
|
+
return `\u001B]8;;${url}\u0007${label}\u001B]8;;\u0007`;
|
|
227
|
+
}
|
|
228
|
+
function normalizeOcrText(value) {
|
|
229
|
+
if (!value)
|
|
230
|
+
return undefined;
|
|
231
|
+
const normalized = value
|
|
232
|
+
.replace(/\r\n/g, '\n')
|
|
233
|
+
.replace(/\r/g, '\n')
|
|
234
|
+
.split('\n')
|
|
235
|
+
.map(line => line.trimEnd())
|
|
236
|
+
.join('\n')
|
|
237
|
+
.trim();
|
|
238
|
+
return normalized.length > 0 ? normalized : undefined;
|
|
239
|
+
}
|
|
240
|
+
function extractOcrDescription(image) {
|
|
241
|
+
const direct = normalizeOcrText(image?.ocr?.description);
|
|
242
|
+
if (direct)
|
|
243
|
+
return direct;
|
|
244
|
+
return normalizeOcrText(image?.metadata?.ocr?.description);
|
|
245
|
+
}
|
|
246
|
+
function buildOcrPreview(ocrText, maxLines) {
|
|
247
|
+
const lines = ocrText.split('\n');
|
|
248
|
+
if (lines.length <= maxLines) {
|
|
249
|
+
return { text: ocrText, truncated: false };
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
text: lines.slice(0, maxLines).join('\n'),
|
|
253
|
+
truncated: true,
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
function extractObjectAnnotations(image) {
|
|
257
|
+
const rawAnnotations = image?.localizedObjectAnnotations ||
|
|
258
|
+
image?.localized_object_annotations ||
|
|
259
|
+
image?.metadata?.localizedObjectAnnotations ||
|
|
260
|
+
image?.metadata?.localized_object_annotations ||
|
|
261
|
+
[];
|
|
262
|
+
if (!Array.isArray(rawAnnotations))
|
|
263
|
+
return [];
|
|
264
|
+
const bestByName = new Map();
|
|
265
|
+
for (const annotation of rawAnnotations) {
|
|
266
|
+
if (!annotation || typeof annotation !== 'object')
|
|
267
|
+
continue;
|
|
268
|
+
const name = normalizeText(annotation.name_ja || annotation.nameJa || annotation.name);
|
|
269
|
+
if (!name)
|
|
270
|
+
continue;
|
|
271
|
+
const score = typeof annotation.score === 'number' ? annotation.score : undefined;
|
|
272
|
+
const existing = bestByName.get(name);
|
|
273
|
+
if (!existing) {
|
|
274
|
+
bestByName.set(name, { name, score });
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
const existingScore = existing.score ?? -1;
|
|
278
|
+
const nextScore = score ?? -1;
|
|
279
|
+
if (nextScore > existingScore) {
|
|
280
|
+
bestByName.set(name, { name, score });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return Array.from(bestByName.values()).sort((a, b) => {
|
|
284
|
+
const sa = a.score ?? -1;
|
|
285
|
+
const sb = b.score ?? -1;
|
|
286
|
+
return sb - sa;
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function formatObjectAnnotationLine(annotation) {
|
|
290
|
+
if (typeof annotation.score === 'number') {
|
|
291
|
+
return `${annotation.name} (${(annotation.score * 100).toFixed(1)}%)`;
|
|
292
|
+
}
|
|
293
|
+
return annotation.name;
|
|
294
|
+
}
|
|
295
|
+
function extractImageApps(image) {
|
|
296
|
+
const app = normalizeText(image?.metadata?.app);
|
|
297
|
+
return app ? [app] : [];
|
|
298
|
+
}
|
|
299
|
+
function extractImageDomains(image) {
|
|
300
|
+
const domain = extractDomain(normalizeText(image?.metadata?.url));
|
|
301
|
+
return domain ? [domain] : [];
|
|
302
|
+
}
|
|
303
|
+
function extractImageLocations(image) {
|
|
304
|
+
const location = normalizeText(extractImageLocationLabel(image));
|
|
305
|
+
return location ? [location] : [];
|
|
306
|
+
}
|
|
307
|
+
function normalizeTagText(value) {
|
|
308
|
+
if (!value)
|
|
309
|
+
return undefined;
|
|
310
|
+
const normalized = normalizeText(value);
|
|
311
|
+
if (!normalized)
|
|
312
|
+
return undefined;
|
|
313
|
+
const stripped = normalized.replace(/^[##]+/, '').trim();
|
|
314
|
+
return stripped.length > 0 ? stripped : undefined;
|
|
315
|
+
}
|
|
316
|
+
function extractTagFromLinkValue(value) {
|
|
317
|
+
if (typeof value === 'string') {
|
|
318
|
+
return normalizeTagText(value);
|
|
319
|
+
}
|
|
320
|
+
if (!value || typeof value !== 'object')
|
|
321
|
+
return undefined;
|
|
322
|
+
const candidates = [
|
|
323
|
+
value.tag,
|
|
324
|
+
value.name,
|
|
325
|
+
value.title,
|
|
326
|
+
value.text,
|
|
327
|
+
value.keyword,
|
|
328
|
+
];
|
|
329
|
+
for (const candidate of candidates) {
|
|
330
|
+
const tag = normalizeTagText(candidate);
|
|
331
|
+
if (tag)
|
|
332
|
+
return tag;
|
|
333
|
+
}
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
function extractImageTags(image) {
|
|
337
|
+
const rawLinks = image?.metadata?.links ?? image?.links;
|
|
338
|
+
if (!Array.isArray(rawLinks))
|
|
339
|
+
return [];
|
|
340
|
+
const tags = [];
|
|
341
|
+
for (const rawLink of rawLinks) {
|
|
342
|
+
const tag = extractTagFromLinkValue(rawLink);
|
|
343
|
+
if (tag)
|
|
344
|
+
tags.push(tag);
|
|
345
|
+
}
|
|
346
|
+
return normalizeRankingValues(tags);
|
|
347
|
+
}
|
|
348
|
+
function normalizeRankingValues(values) {
|
|
349
|
+
const uniqueByLower = new Map();
|
|
350
|
+
for (const raw of values) {
|
|
351
|
+
const value = normalizeText(raw);
|
|
352
|
+
if (!value)
|
|
353
|
+
continue;
|
|
354
|
+
const key = value.toLocaleLowerCase();
|
|
355
|
+
if (!uniqueByLower.has(key)) {
|
|
356
|
+
uniqueByLower.set(key, value);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
return Array.from(uniqueByLower.values());
|
|
360
|
+
}
|
|
361
|
+
function shouldEnrichForLocationDisplay(img) {
|
|
362
|
+
const locationLabel = sanitizeSummaryText(extractImageLocationLabel(img));
|
|
363
|
+
return !locationLabel;
|
|
364
|
+
}
|
|
365
|
+
function mergeImageForDisplay(base, detail) {
|
|
366
|
+
return {
|
|
367
|
+
...base,
|
|
368
|
+
...detail,
|
|
369
|
+
metadata: {
|
|
370
|
+
...(base?.metadata || {}),
|
|
371
|
+
...(detail?.metadata || {}),
|
|
372
|
+
},
|
|
373
|
+
ocr: detail?.ocr ?? base?.ocr,
|
|
374
|
+
};
|
|
375
|
+
}
|
package/dist/ids.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Turning what a human or an agent typed into a Gyazo ID. Kept apart from the
|
|
4
|
+
* CLI itself so that other entry points, such as the MCP server, can reuse it
|
|
5
|
+
* without loading commander and its commands.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.normalizeImageId = normalizeImageId;
|
|
9
|
+
exports.normalizeCollectionId = normalizeCollectionId;
|
|
10
|
+
const IMAGE_ID_PATTERN = /^[0-9a-f]{32}$/i;
|
|
11
|
+
const GYAZO_HOST_PATTERN = /(^|\.)gyazo\.com$/i;
|
|
12
|
+
/**
|
|
13
|
+
* Accept either a bare Gyazo image id (32 hex characters) or any Gyazo URL that
|
|
14
|
+
* carries one, and return the canonical lowercase id. Returns null otherwise.
|
|
15
|
+
*/
|
|
16
|
+
function normalizeImageId(input) {
|
|
17
|
+
const trimmed = (input || '').trim();
|
|
18
|
+
if (!trimmed)
|
|
19
|
+
return null;
|
|
20
|
+
if (IMAGE_ID_PATTERN.test(trimmed)) {
|
|
21
|
+
return trimmed.toLowerCase();
|
|
22
|
+
}
|
|
23
|
+
if (!/^https?:\/\//i.test(trimmed)) {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
let url;
|
|
27
|
+
try {
|
|
28
|
+
url = new URL(trimmed);
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
if (!GYAZO_HOST_PATTERN.test(url.hostname)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
37
|
+
const lastSegment = segments[segments.length - 1];
|
|
38
|
+
if (!lastSegment)
|
|
39
|
+
return null;
|
|
40
|
+
// /collections/<id> is a collection, not an image.
|
|
41
|
+
if (segments[segments.length - 2] === 'collections')
|
|
42
|
+
return null;
|
|
43
|
+
const withoutExtension = lastSegment.replace(/\.[a-z0-9]+$/i, '');
|
|
44
|
+
return IMAGE_ID_PATTERN.test(withoutExtension) ? withoutExtension.toLowerCase() : null;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A collection ID looks exactly like an image ID (32 hex characters), so only
|
|
48
|
+
* the URL form tells the two apart. `/collections/<id>` is a collection;
|
|
49
|
+
* `/<id>` is an image.
|
|
50
|
+
*/
|
|
51
|
+
function normalizeCollectionId(input) {
|
|
52
|
+
const trimmed = (input || '').trim();
|
|
53
|
+
if (!trimmed)
|
|
54
|
+
return null;
|
|
55
|
+
if (IMAGE_ID_PATTERN.test(trimmed)) {
|
|
56
|
+
return trimmed.toLowerCase();
|
|
57
|
+
}
|
|
58
|
+
if (!/^https?:\/\//i.test(trimmed)) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
let url;
|
|
62
|
+
try {
|
|
63
|
+
url = new URL(trimmed);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (!GYAZO_HOST_PATTERN.test(url.hostname)) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
const segments = url.pathname.split('/').filter(Boolean);
|
|
72
|
+
if (segments.length < 2 || segments[segments.length - 2] !== 'collections') {
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
const withoutExtension = segments[segments.length - 1].replace(/\.[a-z0-9]+$/i, '');
|
|
76
|
+
return IMAGE_ID_PATTERN.test(withoutExtension) ? withoutExtension.toLowerCase() : null;
|
|
77
|
+
}
|