Files
CloudFlare-ImgBed/functions/utils/tagHelpers.js
T
sean908 797f63b00a Feat: Add tag management system
- 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
2025-10-10 14:43:11 +08:00

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);
}