mirror of
https://github.com/ZSCGR/CloudFlare-ImgBed.git
synced 2026-08-18 22:53:43 +08:00
- Add tag CRUD APIs (single file and batch operations) - Add tag autocomplete endpoint - Add tag search support in file listing - Update database schema with tags column - Add tag validation and normalization utilities - Initialize Tags:[] for all new uploads
162 lines
4.9 KiB
JavaScript
162 lines
4.9 KiB
JavaScript
/**
|
|
* Tag Management Helper Functions
|
|
* Provides utilities for validating, normalizing, and managing tags
|
|
*/
|
|
|
|
/**
|
|
* Validate tag format
|
|
* Tags must contain only alphanumeric characters, underscores, and hyphens
|
|
* @param {string} tag - The tag to validate
|
|
* @returns {boolean} - Whether the tag is valid
|
|
*/
|
|
export function validateTag(tag) {
|
|
if (!tag || typeof tag !== 'string') {
|
|
return false;
|
|
}
|
|
|
|
// Allow alphanumeric, underscore, hyphen, and Chinese/Japanese/Korean characters
|
|
return /^[\w\u4e00-\u9fa5\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af-]+$/.test(tag);
|
|
}
|
|
|
|
/**
|
|
* Normalize tags
|
|
* - Convert to lowercase
|
|
* - Trim whitespace
|
|
* - Remove duplicates
|
|
* - Filter out invalid tags
|
|
* @param {string[]} tags - Array of tags to normalize
|
|
* @returns {string[]} - Normalized array of unique tags
|
|
*/
|
|
export function normalizeTags(tags) {
|
|
if (!Array.isArray(tags)) {
|
|
return [];
|
|
}
|
|
|
|
const normalized = tags
|
|
.filter(tag => tag && typeof tag === 'string')
|
|
.map(tag => tag.toLowerCase().trim())
|
|
.filter(tag => validateTag(tag));
|
|
|
|
// Remove duplicates while preserving order
|
|
return [...new Set(normalized)];
|
|
}
|
|
|
|
/**
|
|
* Merge tags based on action
|
|
* @param {string[]} existingTags - Current tags on the file
|
|
* @param {string[]} newTags - Tags to add/remove/set
|
|
* @param {string} action - 'set', 'add', or 'remove'
|
|
* @returns {string[]} - Merged tags array
|
|
*/
|
|
export function mergeTags(existingTags, newTags, action) {
|
|
const existing = Array.isArray(existingTags) ? existingTags : [];
|
|
const normalized = normalizeTags(newTags);
|
|
|
|
switch (action) {
|
|
case 'set':
|
|
// Replace all tags with new tags
|
|
return normalized;
|
|
|
|
case 'add':
|
|
// Add new tags to existing, remove duplicates
|
|
return normalizeTags([...existing, ...normalized]);
|
|
|
|
case 'remove':
|
|
// Remove specified tags from existing
|
|
const toRemove = new Set(normalized);
|
|
return existing.filter(tag => !toRemove.has(tag.toLowerCase()));
|
|
|
|
default:
|
|
throw new Error(`Invalid action: ${action}. Must be 'set', 'add', or 'remove'`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse search query to extract tags and keywords
|
|
* Input: "vacation #photo #2024"
|
|
* Output: { keywords: "vacation", tags: ["photo", "2024"] }
|
|
* @param {string} searchString - The search query string
|
|
* @returns {Object} - Object with keywords and tags arrays
|
|
*/
|
|
export function parseSearchQuery(searchString) {
|
|
if (!searchString || typeof searchString !== 'string') {
|
|
return { keywords: '', tags: [] };
|
|
}
|
|
|
|
const tagRegex = /#([\w\u4e00-\u9fa5\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af-]+)/g;
|
|
const tags = [];
|
|
let match;
|
|
|
|
while ((match = tagRegex.exec(searchString)) !== null) {
|
|
tags.push(match[1].toLowerCase());
|
|
}
|
|
|
|
// Remove tags from search string to get keywords
|
|
const keywords = searchString.replace(/#[\w\u4e00-\u9fa5\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af-]+/g, '').trim();
|
|
|
|
return { keywords, tags: normalizeTags(tags) };
|
|
}
|
|
|
|
/**
|
|
* Check if a file matches tag filter
|
|
* @param {string[]} fileTags - Tags on the file
|
|
* @param {string[]} requiredTags - Tags that must be present
|
|
* @returns {boolean} - Whether file has all required tags
|
|
*/
|
|
export function matchesTags(fileTags, requiredTags) {
|
|
if (!Array.isArray(requiredTags) || requiredTags.length === 0) {
|
|
return true; // No tag filter
|
|
}
|
|
|
|
if (!Array.isArray(fileTags) || fileTags.length === 0) {
|
|
return false; // File has no tags but filter requires tags
|
|
}
|
|
|
|
const fileTagsLower = fileTags.map(t => t.toLowerCase());
|
|
return requiredTags.every(tag => fileTagsLower.includes(tag.toLowerCase()));
|
|
}
|
|
|
|
/**
|
|
* Extract all unique tags from an array of files
|
|
* @param {Array} files - Array of file objects with metadata.Tags
|
|
* @returns {string[]} - Sorted array of unique tags
|
|
*/
|
|
export function extractUniqueTags(files) {
|
|
if (!Array.isArray(files)) {
|
|
return [];
|
|
}
|
|
|
|
const allTags = new Set();
|
|
|
|
files.forEach(file => {
|
|
if (file && file.metadata && Array.isArray(file.metadata.Tags)) {
|
|
file.metadata.Tags.forEach(tag => {
|
|
if (tag && typeof tag === 'string') {
|
|
allTags.add(tag.toLowerCase().trim());
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
return Array.from(allTags).sort();
|
|
}
|
|
|
|
/**
|
|
* Filter tags by prefix (for autocomplete)
|
|
* @param {string[]} tags - Array of all available tags
|
|
* @param {string} prefix - Prefix to filter by
|
|
* @param {number} limit - Maximum number of results
|
|
* @returns {string[]} - Filtered tags
|
|
*/
|
|
export function filterTagsByPrefix(tags, prefix, limit = 20) {
|
|
if (!Array.isArray(tags) || !prefix || typeof prefix !== 'string') {
|
|
return [];
|
|
}
|
|
|
|
const prefixLower = prefix.toLowerCase().trim();
|
|
|
|
return tags
|
|
.filter(tag => tag.toLowerCase().startsWith(prefixLower))
|
|
.slice(0, limit);
|
|
}
|