/** * Insertr Content Manager Module * Handles content operations, storage, and API interactions */ class InsertrContentManager { constructor(options = {}) { this.options = { apiEndpoint: options.apiEndpoint || '/api/content', storageKey: options.storageKey || 'insertr_content', ...options }; this.contentCache = new Map(); this.loadContentFromStorage(); } /** * Load content from localStorage */ loadContentFromStorage() { try { const stored = localStorage.getItem(this.options.storageKey); if (stored) { const data = JSON.parse(stored); this.contentCache = new Map(Object.entries(data)); } } catch (error) { console.warn('Failed to load content from storage:', error); } } /** * Save content to localStorage */ saveContentToStorage() { try { const data = Object.fromEntries(this.contentCache); localStorage.setItem(this.options.storageKey, JSON.stringify(data)); } catch (error) { console.warn('Failed to save content to storage:', error); } } /** * Get content for a specific element * @param {string} contentId - Content identifier * @returns {string|Object} Content data */ getContent(contentId) { return this.contentCache.get(contentId); } /** * Set content for an element * @param {string} contentId - Content identifier * @param {string|Object} content - Content data */ setContent(contentId, content) { this.contentCache.set(contentId, content); this.saveContentToStorage(); } /** * Apply content to DOM element * @param {HTMLElement} element - Element to update * @param {string|Object} content - Content to apply */ applyContentToElement(element, content) { const config = element._insertrConfig; if (config.type === 'markdown') { // Handle markdown collection - content is a string this.applyMarkdownContent(element, content); } else if (config.type === 'link' && config.includeUrl && content.url !== undefined) { // Update link text and URL element.textContent = this.sanitizeForDisplay(content.text, 'text') || element.textContent; if (content.url) { element.href = this.sanitizeForDisplay(content.url, 'url'); } } else if (content.text !== undefined) { // Update text content element.textContent = this.sanitizeForDisplay(content.text, 'text'); } } /** * Apply markdown content to element * @param {HTMLElement} element - Element to update * @param {string} markdownText - Markdown content */ applyMarkdownContent(element, markdownText) { // This method will be implemented by the main Insertr class // which has access to the markdown processor if (window.insertr && window.insertr.renderMarkdown) { window.insertr.renderMarkdown(element, markdownText); } else { console.warn('Markdown processor not available'); element.textContent = markdownText; } } /** * Extract content from DOM element * @param {HTMLElement} element - Element to extract from * @returns {string|Object} Extracted content */ extractContentFromElement(element) { const config = element._insertrConfig; if (config.type === 'markdown') { // For markdown collections, return cached content or extract text const contentId = element.getAttribute('data-content-id'); const cached = this.contentCache.get(contentId); if (cached) { // Handle both old format (object) and new format (string) if (typeof cached === 'string') { return cached; } else if (cached.text && typeof cached.text === 'string') { return cached.text; } } // Fallback: extract basic text content const clone = element.cloneNode(true); const editBtn = clone.querySelector('.insertr-edit-btn'); if (editBtn) editBtn.remove(); // Convert basic HTML structure to markdown return this.basicHtmlToMarkdown(clone.innerHTML); } // Clone element to avoid modifying original const clone = element.cloneNode(true); // Remove edit button from clone const editBtn = clone.querySelector('.insertr-edit-btn'); if (editBtn) editBtn.remove(); // Extract content based on element type if (config.type === 'link' && config.includeUrl) { return { text: clone.textContent.trim(), url: element.href || '' }; } return clone.textContent.trim(); } /** * Convert basic HTML to markdown (for initial content extraction) * @param {string} html - HTML content * @returns {string} Markdown content */ basicHtmlToMarkdown(html) { let markdown = html; // Basic paragraph conversion markdown = markdown.replace(/]*>(.*?)<\/p>/gis, (match, content) => { return content.trim() + '\n\n'; }); markdown = markdown.replace(//gi, '\n'); // Remove HTML tags markdown = markdown.replace(/<[^>]*>/g, ''); // Clean up entities markdown = markdown.replace(/ /g, ' '); markdown = markdown.replace(/&/g, '&'); markdown = markdown.replace(/</g, '<'); markdown = markdown.replace(/>/g, '>'); // Clean whitespace markdown = markdown .split('\n') .map(line => line.trim()) .join('\n') .replace(/\n\n\n+/g, '\n\n') .trim(); return markdown; } /** * Save content to server (mock implementation) * @param {string} contentId - Content identifier * @param {string|Object} content - Content to save * @returns {Promise} Save operation promise */ async saveToServer(contentId, content) { // Mock API call - replace with real implementation try { console.log(`💾 Saving content for ${contentId}:`, content); // Simulate network delay await new Promise(resolve => setTimeout(resolve, 500)); // Store locally for now this.setContent(contentId, content); return { success: true, contentId, content }; } catch (error) { console.error('Failed to save content:', error); throw new Error('Save operation failed'); } } /** * Basic sanitization for display * @param {string} content - Content to sanitize * @param {string} type - Content type * @returns {string} Sanitized content */ sanitizeForDisplay(content, type) { if (!content) return ''; switch (type) { case 'text': return this.escapeHtml(content); case 'url': if (content.startsWith('javascript:') || content.startsWith('data:')) { return ''; } return content; default: return this.escapeHtml(content); } } /** * Escape HTML characters * @param {string} text - Text to escape * @returns {string} Escaped text */ escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } /** * Clear all cached content */ clearCache() { this.contentCache.clear(); localStorage.removeItem(this.options.storageKey); } /** * Get all cached content * @returns {Object} All cached content */ getAllContent() { return Object.fromEntries(this.contentCache); } } // Export for module usage if (typeof module !== 'undefined' && module.exports) { module.exports = InsertrContentManager; } // Global export for browser usage if (typeof window !== 'undefined') { window.InsertrContentManager = InsertrContentManager; }