/** * 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;
}