/** * ContentDB URL Parser and Validator * Handles parsing and validation of ContentDB package URLs */ class ContentDBUrlParser { /** * Parse a ContentDB URL to extract author and package name * @param {string} url - The URL to parse * @returns {Object} - {author, name, isValid, originalUrl} */ static parseUrl(url) { if (!url || typeof url !== 'string') { return { author: null, name: null, isValid: false, originalUrl: url, error: 'URL is required' }; } // Clean up the URL let cleanUrl = url.trim(); // Remove protocol cleanUrl = cleanUrl.replace(/^https?:\/\//, ''); // Remove trailing slash cleanUrl = cleanUrl.replace(/\/$/, ''); // Define patterns to match const patterns = [ // Full ContentDB URL: content.luanti.org/packages/author/name /^content\.luanti\.org\/packages\/([^\/\s]+)\/([^\/\s]+)$/, // Alternative domain patterns (if any) /^(?:www\.)?content\.luanti\.org\/packages\/([^\/\s]+)\/([^\/\s]+)$/, // Direct author/name format /^([^\/\s]+)\/([^\/\s]+)$/ ]; // Try each pattern for (const pattern of patterns) { const match = cleanUrl.match(pattern); if (match) { const author = match[1]; const name = match[2]; // Validate author and name format if (this.isValidIdentifier(author) && this.isValidIdentifier(name)) { return { author: author, name: name, isValid: true, originalUrl: url, cleanUrl: cleanUrl, fullUrl: `https://content.luanti.org/packages/${author}/${name}/` }; } else { return { author: null, name: null, isValid: false, originalUrl: url, error: 'Invalid author or package name format' }; } } } return { author: null, name: null, isValid: false, originalUrl: url, error: 'URL format not recognized' }; } /** * Validate an identifier (author or package name) * @param {string} identifier - The identifier to validate * @returns {boolean} - Whether the identifier is valid */ static isValidIdentifier(identifier) { if (!identifier || typeof identifier !== 'string') { return false; } // ContentDB identifiers should be alphanumeric with underscores and hyphens // Length should be reasonable (3-50 characters) return /^[a-zA-Z0-9_-]{3,50}$/.test(identifier); } /** * Generate various URL formats for a package * @param {string} author - Package author * @param {string} name - Package name * @returns {Object} - Object containing different URL formats */ static generateUrls(author, name) { if (!this.isValidIdentifier(author) || !this.isValidIdentifier(name)) { throw new Error('Invalid author or package name'); } return { web: `https://content.luanti.org/packages/${author}/${name}/`, api: `https://content.luanti.org/api/packages/${author}/${name}/`, releases: `https://content.luanti.org/api/packages/${author}/${name}/releases/`, direct: `${author}/${name}` }; } /** * Validate multiple URL formats and suggest corrections * @param {string} url - The URL to validate * @returns {Object} - Validation result with suggestions */ static validateWithSuggestions(url) { const result = this.parseUrl(url); if (result.isValid) { return { ...result, suggestions: [] }; } // Generate suggestions for common mistakes const suggestions = []; if (url.includes('minetest.') || url.includes('minetest/')) { suggestions.push('Did you mean content.luanti.org instead of minetest?'); } if (url.includes('://content.luanti.org') && !url.includes('/packages/')) { suggestions.push('Make sure the URL includes /packages/author/name/'); } if (url.includes(' ')) { suggestions.push('Remove spaces from the URL'); } // Check if it looks like a partial URL if (url.includes('/') && !url.includes('content.luanti.org')) { suggestions.push('Try the full URL: https://content.luanti.org/packages/author/name/'); } return { ...result, suggestions }; } /** * Extract package information from various URL formats * @param {string} url - The URL to extract from * @returns {Promise} - Package information if available */ static async extractPackageInfo(url) { const parsed = this.parseUrl(url); if (!parsed.isValid) { throw new Error(parsed.error || 'Invalid URL format'); } return { author: parsed.author, name: parsed.name, identifier: `${parsed.author}/${parsed.name}`, urls: this.generateUrls(parsed.author, parsed.name) }; } /** * Check if a URL is a ContentDB package URL * @param {string} url - The URL to check * @returns {boolean} - Whether it's a ContentDB package URL */ static isContentDBUrl(url) { return this.parseUrl(url).isValid; } /** * Normalize a URL to standard format * @param {string} url - The URL to normalize * @returns {string} - Normalized URL */ static normalizeUrl(url) { const parsed = this.parseUrl(url); if (!parsed.isValid) { throw new Error(parsed.error || 'Invalid URL format'); } return parsed.fullUrl; } } module.exports = ContentDBUrlParser;