Refactor to modular architecture with extensible configuration system
- Split monolithic insertr.js (932 lines) into 6 focused modules - Extract configuration system for extensible field types and validation - Separate validation, form rendering, content management, and markdown processing - Maintain same API surface while improving maintainability and testability - Update demo pages to use modular system - Remove legacy support for cleaner codebase
This commit is contained in:
12
README.md
12
README.md
@@ -94,9 +94,17 @@ Perfect for:
|
|||||||
|
|
||||||
### Basic Setup
|
### Basic Setup
|
||||||
```html
|
```html
|
||||||
<!-- Include marked.js for markdown support -->
|
<!-- Include required dependencies -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked@16.2.1/lib/marked.umd.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked@16.2.1/lib/marked.umd.js"></script>
|
||||||
<!-- Include Insertr -->
|
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.5/dist/purify.min.js"></script>
|
||||||
|
|
||||||
|
<!-- Include Insertr modules -->
|
||||||
|
<link rel="stylesheet" href="insertr/insertr.css">
|
||||||
|
<script src="insertr/config.js"></script>
|
||||||
|
<script src="insertr/validation.js"></script>
|
||||||
|
<script src="insertr/form-renderer.js"></script>
|
||||||
|
<script src="insertr/content-manager.js"></script>
|
||||||
|
<script src="insertr/markdown-processor.js"></script>
|
||||||
<script src="insertr/insertr.js"></script>
|
<script src="insertr/insertr.js"></script>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -58,21 +58,21 @@
|
|||||||
<div class="insertr" data-content-id="member1-info" data-field-type="markdown">
|
<div class="insertr" data-content-id="member1-info" data-field-type="markdown">
|
||||||
<h3>Sarah Chen</h3>
|
<h3>Sarah Chen</h3>
|
||||||
<p><strong>Founder & CEO</strong></p>
|
<p><strong>Founder & CEO</strong></p>
|
||||||
<p>Former **McKinsey consultant** with 15 years of experience in strategy and operations. MBA from Stanford.</p>
|
<p>Former <strong>McKinsey consultant</strong> with 15 years of experience in strategy and operations. MBA from Stanford.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="service-card">
|
<div class="service-card">
|
||||||
<div class="insertr" data-content-id="member2-info" data-field-type="markdown">
|
<div class="insertr" data-content-id="member2-info" data-field-type="markdown">
|
||||||
<h3>Michael Rodriguez</h3>
|
<h3>Michael Rodriguez</h3>
|
||||||
<p><strong>Head of Operations</strong></p>
|
<p><strong>Head of Operations</strong></p>
|
||||||
<p>20 years in manufacturing and supply chain optimization. Expert in **lean methodologies** and process improvement.</p>
|
<p>20 years in manufacturing and supply chain optimization. Expert in <strong>lean methodologies</strong> and process improvement.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="service-card">
|
<div class="service-card">
|
||||||
<div class="insertr" data-content-id="member3-info" data-field-type="markdown">
|
<div class="insertr" data-content-id="member3-info" data-field-type="markdown">
|
||||||
<h3>Emma Thompson</h3>
|
<h3>Emma Thompson</h3>
|
||||||
<p><strong>Digital Strategy Lead</strong></p>
|
<p><strong>Digital Strategy Lead</strong></p>
|
||||||
<p>Former tech startup founder turned consultant. Specializes in *digital transformation* and technology adoption.</p>
|
<p>Former tech startup founder turned consultant. Specializes in <em>digital transformation</em> and technology adoption.</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -111,6 +111,11 @@
|
|||||||
<!-- Insertr JavaScript Library -->
|
<!-- Insertr JavaScript Library -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked@16.2.1/lib/marked.umd.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked@16.2.1/lib/marked.umd.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.5/dist/purify.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.5/dist/purify.min.js"></script>
|
||||||
|
<script src="insertr/config.js"></script>
|
||||||
|
<script src="insertr/validation.js"></script>
|
||||||
|
<script src="insertr/form-renderer.js"></script>
|
||||||
|
<script src="insertr/content-manager.js"></script>
|
||||||
|
<script src="insertr/markdown-processor.js"></script>
|
||||||
<script src="insertr/insertr.js"></script>
|
<script src="insertr/insertr.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -87,6 +87,11 @@
|
|||||||
<!-- Insertr JavaScript Library -->
|
<!-- Insertr JavaScript Library -->
|
||||||
<script src="https://cdn.jsdelivr.net/npm/marked@16.2.1/lib/marked.umd.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/marked@16.2.1/lib/marked.umd.js"></script>
|
||||||
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.5/dist/purify.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/dompurify@3.0.5/dist/purify.min.js"></script>
|
||||||
|
<script src="insertr/config.js"></script>
|
||||||
|
<script src="insertr/validation.js"></script>
|
||||||
|
<script src="insertr/form-renderer.js"></script>
|
||||||
|
<script src="insertr/content-manager.js"></script>
|
||||||
|
<script src="insertr/markdown-processor.js"></script>
|
||||||
<script src="insertr/insertr.js"></script>
|
<script src="insertr/insertr.js"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
175
demo-site/insertr/config.js
Normal file
175
demo-site/insertr/config.js
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
/**
|
||||||
|
* Insertr Configuration System
|
||||||
|
* Extensible field type detection and form configuration
|
||||||
|
*/
|
||||||
|
|
||||||
|
class InsertrConfig {
|
||||||
|
constructor(customConfig = {}) {
|
||||||
|
// Default field type mappings
|
||||||
|
this.defaultFieldTypes = {
|
||||||
|
'H1': { type: 'text', label: 'Headline', maxLength: 60, placeholder: 'Enter headline...' },
|
||||||
|
'H2': { type: 'text', label: 'Subheading', maxLength: 80, placeholder: 'Enter subheading...' },
|
||||||
|
'H3': { type: 'text', label: 'Section Title', maxLength: 100, placeholder: 'Enter title...' },
|
||||||
|
'H4': { type: 'text', label: 'Title', maxLength: 100, placeholder: 'Enter title...' },
|
||||||
|
'H5': { type: 'text', label: 'Title', maxLength: 100, placeholder: 'Enter title...' },
|
||||||
|
'H6': { type: 'text', label: 'Title', maxLength: 100, placeholder: 'Enter title...' },
|
||||||
|
'P': { type: 'textarea', label: 'Paragraph', rows: 3, placeholder: 'Enter paragraph text...' },
|
||||||
|
'A': { type: 'link', label: 'Link', placeholder: 'Enter link text...' },
|
||||||
|
'SPAN': { type: 'text', label: 'Text', placeholder: 'Enter text...' },
|
||||||
|
'CITE': { type: 'text', label: 'Citation', placeholder: 'Enter citation...' },
|
||||||
|
'BUTTON': { type: 'text', label: 'Button Text', placeholder: 'Enter button text...' }
|
||||||
|
};
|
||||||
|
|
||||||
|
// CSS class-based enhancements
|
||||||
|
this.classEnhancements = {
|
||||||
|
'lead': {
|
||||||
|
label: 'Lead Paragraph',
|
||||||
|
rows: 4,
|
||||||
|
placeholder: 'Enter lead paragraph...'
|
||||||
|
},
|
||||||
|
'btn-primary': {
|
||||||
|
type: 'link',
|
||||||
|
label: 'Primary Button',
|
||||||
|
includeUrl: true,
|
||||||
|
placeholder: 'Enter button text...'
|
||||||
|
},
|
||||||
|
'btn-secondary': {
|
||||||
|
type: 'link',
|
||||||
|
label: 'Secondary Button',
|
||||||
|
includeUrl: true,
|
||||||
|
placeholder: 'Enter button text...'
|
||||||
|
},
|
||||||
|
'section-subtitle': {
|
||||||
|
label: 'Section Subtitle',
|
||||||
|
placeholder: 'Enter subtitle...'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Content ID-based enhancements
|
||||||
|
this.contentIdRules = [
|
||||||
|
{
|
||||||
|
pattern: /cta/i,
|
||||||
|
config: { label: 'Call to Action' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
pattern: /quote/i,
|
||||||
|
config: {
|
||||||
|
type: 'textarea',
|
||||||
|
rows: 3,
|
||||||
|
label: 'Quote',
|
||||||
|
placeholder: 'Enter quote...'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
// Validation limits
|
||||||
|
this.limits = {
|
||||||
|
maxContentLength: 10000,
|
||||||
|
maxHtmlTags: 20,
|
||||||
|
...customConfig.limits
|
||||||
|
};
|
||||||
|
|
||||||
|
// Markdown configuration
|
||||||
|
this.markdown = {
|
||||||
|
enabled: true,
|
||||||
|
label: 'Content (Markdown)',
|
||||||
|
rows: 8,
|
||||||
|
placeholder: 'Enter content in Markdown format...\n\nUse **bold**, *italic*, [links](url), and double line breaks for new paragraphs.',
|
||||||
|
...customConfig.markdown
|
||||||
|
};
|
||||||
|
|
||||||
|
// Merge custom configurations
|
||||||
|
this.fieldTypes = { ...this.defaultFieldTypes, ...customConfig.fieldTypes };
|
||||||
|
this.classEnhancements = { ...this.classEnhancements, ...customConfig.classEnhancements };
|
||||||
|
|
||||||
|
if (customConfig.contentIdRules) {
|
||||||
|
this.contentIdRules = [...this.contentIdRules, ...customConfig.contentIdRules];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate field configuration for an element
|
||||||
|
* @param {HTMLElement} element - The element to configure
|
||||||
|
* @returns {Object} Field configuration
|
||||||
|
*/
|
||||||
|
generateFieldConfig(element) {
|
||||||
|
// Check for explicit markdown type first
|
||||||
|
const fieldType = element.getAttribute('data-field-type');
|
||||||
|
if (fieldType === 'markdown') {
|
||||||
|
return { type: 'markdown', ...this.markdown };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start with tag-based configuration
|
||||||
|
const tagName = element.tagName;
|
||||||
|
let config = { ...this.fieldTypes[tagName] } || {
|
||||||
|
type: 'text',
|
||||||
|
label: 'Content',
|
||||||
|
placeholder: 'Enter content...'
|
||||||
|
};
|
||||||
|
|
||||||
|
// Apply class-based enhancements
|
||||||
|
for (const [className, enhancement] of Object.entries(this.classEnhancements)) {
|
||||||
|
if (element.classList.contains(className)) {
|
||||||
|
config = { ...config, ...enhancement };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply content ID-based rules
|
||||||
|
const contentId = element.getAttribute('data-content-id');
|
||||||
|
if (contentId) {
|
||||||
|
for (const rule of this.contentIdRules) {
|
||||||
|
if (rule.pattern.test(contentId)) {
|
||||||
|
config = { ...config, ...rule.config };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add or override field type mapping
|
||||||
|
* @param {string} tagName - HTML tag name
|
||||||
|
* @param {Object} config - Field configuration
|
||||||
|
*/
|
||||||
|
addFieldType(tagName, config) {
|
||||||
|
this.fieldTypes[tagName.toUpperCase()] = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add class-based enhancement
|
||||||
|
* @param {string} className - CSS class name
|
||||||
|
* @param {Object} enhancement - Configuration enhancement
|
||||||
|
*/
|
||||||
|
addClassEnhancement(className, enhancement) {
|
||||||
|
this.classEnhancements[className] = enhancement;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add content ID rule
|
||||||
|
* @param {RegExp|string} pattern - Pattern to match content IDs
|
||||||
|
* @param {Object} config - Configuration to apply
|
||||||
|
*/
|
||||||
|
addContentIdRule(pattern, config) {
|
||||||
|
const regexPattern = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'i');
|
||||||
|
this.contentIdRules.push({ pattern: regexPattern, config });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get validation limits
|
||||||
|
* @returns {Object} Validation limits
|
||||||
|
*/
|
||||||
|
getValidationLimits() {
|
||||||
|
return { ...this.limits };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for module usage
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = InsertrConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global export for browser usage
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.InsertrConfig = InsertrConfig;
|
||||||
|
}
|
||||||
268
demo-site/insertr/content-manager.js
Normal file
268
demo-site/insertr/content-manager.js
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
/**
|
||||||
|
* 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[^>]*>(.*?)<\/p>/gis, (match, content) => {
|
||||||
|
return content.trim() + '\n\n';
|
||||||
|
});
|
||||||
|
markdown = markdown.replace(/<br\s*\/?>/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;
|
||||||
|
}
|
||||||
305
demo-site/insertr/form-renderer.js
Normal file
305
demo-site/insertr/form-renderer.js
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
/**
|
||||||
|
* Insertr Form Renderer Module
|
||||||
|
* Handles form creation and UI interactions
|
||||||
|
*/
|
||||||
|
|
||||||
|
class InsertrFormRenderer {
|
||||||
|
constructor(validation) {
|
||||||
|
this.validation = validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create edit form for a content element
|
||||||
|
* @param {string} contentId - Content identifier
|
||||||
|
* @param {Object} config - Field configuration
|
||||||
|
* @param {string|Object} currentContent - Current content value
|
||||||
|
* @returns {HTMLElement} Form element
|
||||||
|
*/
|
||||||
|
createEditForm(contentId, config, currentContent) {
|
||||||
|
const form = document.createElement('div');
|
||||||
|
form.className = 'insertr-edit-form';
|
||||||
|
|
||||||
|
let formHTML = `<div class="insertr-form-header">${config.label}</div>`;
|
||||||
|
|
||||||
|
if (config.type === 'markdown') {
|
||||||
|
// Markdown collection editing
|
||||||
|
formHTML += `
|
||||||
|
<div class="insertr-form-group">
|
||||||
|
<textarea class="insertr-form-textarea insertr-markdown-editor" name="content"
|
||||||
|
rows="${config.rows || 8}"
|
||||||
|
placeholder="${config.placeholder}">${this.validation.escapeHtml(currentContent)}</textarea>
|
||||||
|
<div class="insertr-form-help">
|
||||||
|
Live preview will appear here when you start typing
|
||||||
|
</div>
|
||||||
|
<div class="insertr-markdown-preview" style="display: none;">
|
||||||
|
<div class="insertr-preview-label">Preview:</div>
|
||||||
|
<div class="insertr-preview-content"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (config.type === 'link' && config.includeUrl) {
|
||||||
|
// Link with URL field
|
||||||
|
const linkText = typeof currentContent === 'object' ? currentContent.text || '' : currentContent;
|
||||||
|
const linkUrl = typeof currentContent === 'object' ? currentContent.url || '' : '';
|
||||||
|
|
||||||
|
formHTML += `
|
||||||
|
<div class="insertr-form-group">
|
||||||
|
<label>Link Text:</label>
|
||||||
|
<input type="text" class="insertr-form-input" name="text"
|
||||||
|
value="${this.validation.escapeHtml(linkText)}"
|
||||||
|
placeholder="${config.placeholder}"
|
||||||
|
maxlength="${config.maxLength || 200}">
|
||||||
|
</div>
|
||||||
|
<div class="insertr-form-group">
|
||||||
|
<label>Link URL:</label>
|
||||||
|
<input type="url" class="insertr-form-input" name="url"
|
||||||
|
value="${this.validation.escapeHtml(linkUrl)}"
|
||||||
|
placeholder="https://example.com">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else if (config.type === 'textarea') {
|
||||||
|
// Textarea for longer content
|
||||||
|
const content = typeof currentContent === 'object' ? currentContent.text || '' : currentContent;
|
||||||
|
formHTML += `
|
||||||
|
<div class="insertr-form-group">
|
||||||
|
<textarea class="insertr-form-textarea" name="content"
|
||||||
|
rows="${config.rows || 3}"
|
||||||
|
placeholder="${config.placeholder}"
|
||||||
|
maxlength="${config.maxLength || 1000}">${this.validation.escapeHtml(content)}</textarea>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Regular text input
|
||||||
|
const content = typeof currentContent === 'object' ? currentContent.text || '' : currentContent;
|
||||||
|
formHTML += `
|
||||||
|
<div class="insertr-form-group">
|
||||||
|
<input type="text" class="insertr-form-input" name="content"
|
||||||
|
value="${this.validation.escapeHtml(content)}"
|
||||||
|
placeholder="${config.placeholder}"
|
||||||
|
maxlength="${config.maxLength || 200}">
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Form buttons
|
||||||
|
formHTML += `
|
||||||
|
<div class="insertr-form-actions">
|
||||||
|
<button type="button" class="insertr-btn-save">Save</button>
|
||||||
|
<button type="button" class="insertr-btn-cancel">Cancel</button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
form.innerHTML = formHTML;
|
||||||
|
|
||||||
|
// Setup form validation
|
||||||
|
this.setupFormValidation(form, config);
|
||||||
|
|
||||||
|
return form;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup real-time validation for form inputs
|
||||||
|
* @param {HTMLElement} form - Form element
|
||||||
|
* @param {Object} config - Field configuration
|
||||||
|
*/
|
||||||
|
setupFormValidation(form, config) {
|
||||||
|
const inputs = form.querySelectorAll('input, textarea');
|
||||||
|
|
||||||
|
inputs.forEach(input => {
|
||||||
|
// Real-time validation on input
|
||||||
|
input.addEventListener('input', () => {
|
||||||
|
this.validateFormField(input, config);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Also validate on blur for better UX
|
||||||
|
input.addEventListener('blur', () => {
|
||||||
|
this.validateFormField(input, config);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup markdown preview if applicable
|
||||||
|
if (config.type === 'markdown') {
|
||||||
|
this.setupMarkdownPreview(form);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate individual form field
|
||||||
|
* @param {HTMLElement} input - Input element to validate
|
||||||
|
* @param {Object} config - Field configuration
|
||||||
|
*/
|
||||||
|
validateFormField(input, config) {
|
||||||
|
const value = input.value.trim();
|
||||||
|
let fieldType = config.type;
|
||||||
|
|
||||||
|
// Determine validation type based on input
|
||||||
|
if (input.type === 'url' || input.name === 'url') {
|
||||||
|
fieldType = 'link';
|
||||||
|
}
|
||||||
|
|
||||||
|
const validation = this.validation.validateInput(value, fieldType);
|
||||||
|
|
||||||
|
// Visual feedback
|
||||||
|
input.classList.toggle('error', !validation.valid);
|
||||||
|
input.classList.toggle('valid', validation.valid && value.length > 0);
|
||||||
|
|
||||||
|
// Show/hide validation message
|
||||||
|
if (!validation.valid) {
|
||||||
|
this.validation.showValidationMessage(input, validation.message, true);
|
||||||
|
} else {
|
||||||
|
this.validation.showValidationMessage(input, '', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return validation.valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Setup live markdown preview
|
||||||
|
* @param {HTMLElement} form - Form containing markdown textarea
|
||||||
|
*/
|
||||||
|
setupMarkdownPreview(form) {
|
||||||
|
const textarea = form.querySelector('.insertr-markdown-editor');
|
||||||
|
const preview = form.querySelector('.insertr-markdown-preview');
|
||||||
|
const previewContent = form.querySelector('.insertr-preview-content');
|
||||||
|
|
||||||
|
if (!textarea || !preview || !previewContent) return;
|
||||||
|
|
||||||
|
let previewTimeout;
|
||||||
|
|
||||||
|
textarea.addEventListener('input', () => {
|
||||||
|
const content = textarea.value.trim();
|
||||||
|
|
||||||
|
if (content) {
|
||||||
|
preview.style.display = 'block';
|
||||||
|
|
||||||
|
// Debounced preview update
|
||||||
|
clearTimeout(previewTimeout);
|
||||||
|
previewTimeout = setTimeout(() => {
|
||||||
|
this.updateMarkdownPreview(previewContent, content);
|
||||||
|
}, 300);
|
||||||
|
} else {
|
||||||
|
preview.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update markdown preview content
|
||||||
|
* @param {HTMLElement} previewElement - Preview container
|
||||||
|
* @param {string} markdown - Markdown content
|
||||||
|
*/
|
||||||
|
updateMarkdownPreview(previewElement, markdown) {
|
||||||
|
// This method will be called by the main Insertr class
|
||||||
|
// which has access to the markdown processor
|
||||||
|
if (window.insertr && window.insertr.updateMarkdownPreview) {
|
||||||
|
window.insertr.updateMarkdownPreview(previewElement, markdown);
|
||||||
|
} else {
|
||||||
|
previewElement.innerHTML = '<p><em>Preview unavailable</em></p>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position edit form relative to element
|
||||||
|
* @param {HTMLElement} element - Element being edited
|
||||||
|
* @param {HTMLElement} overlay - Form overlay
|
||||||
|
*/
|
||||||
|
positionEditForm(element, overlay) {
|
||||||
|
const rect = element.getBoundingClientRect();
|
||||||
|
const form = overlay.querySelector('.insertr-edit-form');
|
||||||
|
|
||||||
|
// Calculate optimal width (responsive)
|
||||||
|
const viewportWidth = window.innerWidth;
|
||||||
|
let formWidth;
|
||||||
|
|
||||||
|
if (viewportWidth < 768) {
|
||||||
|
formWidth = Math.min(viewportWidth - 40, 350);
|
||||||
|
} else {
|
||||||
|
formWidth = Math.min(Math.max(rect.width, 300), 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
form.style.width = `${formWidth}px`;
|
||||||
|
|
||||||
|
// Position below element with some spacing
|
||||||
|
const top = rect.bottom + window.scrollY + 10;
|
||||||
|
const left = Math.max(20, rect.left + window.scrollX);
|
||||||
|
|
||||||
|
overlay.style.position = 'absolute';
|
||||||
|
overlay.style.top = `${top}px`;
|
||||||
|
overlay.style.left = `${left}px`;
|
||||||
|
overlay.style.zIndex = '10000';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show edit form
|
||||||
|
* @param {HTMLElement} element - Element being edited
|
||||||
|
* @param {HTMLElement} form - Form to show
|
||||||
|
*/
|
||||||
|
showEditForm(element, form) {
|
||||||
|
// Create overlay
|
||||||
|
const overlay = document.createElement('div');
|
||||||
|
overlay.className = 'insertr-form-overlay';
|
||||||
|
overlay.appendChild(form);
|
||||||
|
|
||||||
|
// Position and show
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
this.positionEditForm(element, overlay);
|
||||||
|
|
||||||
|
// Focus first input
|
||||||
|
const firstInput = form.querySelector('input, textarea');
|
||||||
|
if (firstInput) {
|
||||||
|
setTimeout(() => firstInput.focus(), 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle clicking outside to close
|
||||||
|
overlay.addEventListener('click', (e) => {
|
||||||
|
if (e.target === overlay) {
|
||||||
|
this.hideEditForm(overlay);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return overlay;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide edit form
|
||||||
|
* @param {HTMLElement} overlay - Form overlay to hide
|
||||||
|
*/
|
||||||
|
hideEditForm(overlay) {
|
||||||
|
if (overlay && overlay.parentNode) {
|
||||||
|
overlay.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract form data
|
||||||
|
* @param {HTMLElement} form - Form to extract data from
|
||||||
|
* @param {Object} config - Field configuration
|
||||||
|
* @returns {Object|string} Extracted form data
|
||||||
|
*/
|
||||||
|
extractFormData(form, config) {
|
||||||
|
if (config.type === 'markdown') {
|
||||||
|
const textarea = form.querySelector('[name="content"]');
|
||||||
|
return textarea ? textarea.value.trim() : '';
|
||||||
|
} else if (config.type === 'link' && config.includeUrl) {
|
||||||
|
const textInput = form.querySelector('[name="text"]');
|
||||||
|
const urlInput = form.querySelector('[name="url"]');
|
||||||
|
return {
|
||||||
|
text: textInput ? textInput.value.trim() : '',
|
||||||
|
url: urlInput ? urlInput.value.trim() : ''
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
const input = form.querySelector('[name="content"]');
|
||||||
|
return input ? input.value.trim() : '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for module usage
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = InsertrFormRenderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global export for browser usage
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.InsertrFormRenderer = InsertrFormRenderer;
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
194
demo-site/insertr/markdown-processor.js
Normal file
194
demo-site/insertr/markdown-processor.js
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* Insertr Markdown Processor Module
|
||||||
|
* Handles markdown parsing and rendering with sanitization
|
||||||
|
*/
|
||||||
|
|
||||||
|
class InsertrMarkdownProcessor {
|
||||||
|
constructor() {
|
||||||
|
this.marked = null;
|
||||||
|
this.markedParser = null;
|
||||||
|
this.DOMPurify = null;
|
||||||
|
|
||||||
|
this.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize markdown processor
|
||||||
|
*/
|
||||||
|
initialize() {
|
||||||
|
// Check if marked is available
|
||||||
|
if (typeof marked === 'undefined' && typeof window.marked === 'undefined') {
|
||||||
|
console.warn('Marked.js not loaded! Markdown collections will not work.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the marked object
|
||||||
|
this.marked = window.marked || marked;
|
||||||
|
this.markedParser = this.marked.marked || this.marked.parse || this.marked;
|
||||||
|
|
||||||
|
if (typeof this.markedParser !== 'function') {
|
||||||
|
console.warn('Cannot find marked parse function');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Configure marked for basic use
|
||||||
|
if (this.marked.use) {
|
||||||
|
this.marked.use({
|
||||||
|
breaks: true,
|
||||||
|
gfm: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize DOMPurify if available
|
||||||
|
if (typeof DOMPurify !== 'undefined' || typeof window.DOMPurify !== 'undefined') {
|
||||||
|
this.DOMPurify = window.DOMPurify || DOMPurify;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✅ Markdown processor initialized');
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if markdown processor is ready
|
||||||
|
* @returns {boolean} Ready status
|
||||||
|
*/
|
||||||
|
isReady() {
|
||||||
|
return this.markedParser !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render markdown to HTML
|
||||||
|
* @param {string} markdownText - Markdown content
|
||||||
|
* @returns {string} Rendered HTML
|
||||||
|
*/
|
||||||
|
renderToHtml(markdownText) {
|
||||||
|
if (!this.markedParser) {
|
||||||
|
console.error('Markdown parser not available');
|
||||||
|
return markdownText;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof markdownText !== 'string') {
|
||||||
|
console.error('Expected markdown string, got:', typeof markdownText, markdownText);
|
||||||
|
return 'Markdown content error';
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Convert markdown to HTML
|
||||||
|
const html = this.markedParser(markdownText);
|
||||||
|
|
||||||
|
if (typeof html !== 'string') {
|
||||||
|
console.error('Markdown parser returned non-string:', typeof html, html);
|
||||||
|
return 'Markdown parsing error';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize HTML if DOMPurify is available
|
||||||
|
if (this.DOMPurify) {
|
||||||
|
return this.DOMPurify.sanitize(html, {
|
||||||
|
ALLOWED_TAGS: ['p', 'strong', 'em', 'a', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'ul', 'ol', 'li', 'br'],
|
||||||
|
ALLOWED_ATTR: ['href', 'class'],
|
||||||
|
ALLOWED_SCHEMES: ['http', 'https', 'mailto']
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Markdown rendering failed:', error);
|
||||||
|
return markdownText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply markdown content to DOM element
|
||||||
|
* @param {HTMLElement} element - Element to update
|
||||||
|
* @param {string} markdownText - Markdown content
|
||||||
|
*/
|
||||||
|
applyToElement(element, markdownText) {
|
||||||
|
const html = this.renderToHtml(markdownText);
|
||||||
|
element.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create markdown preview
|
||||||
|
* @param {string} markdownText - Markdown content
|
||||||
|
* @returns {string} HTML preview
|
||||||
|
*/
|
||||||
|
createPreview(markdownText) {
|
||||||
|
return this.renderToHtml(markdownText);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate markdown content
|
||||||
|
* @param {string} markdownText - Markdown to validate
|
||||||
|
* @returns {Object} Validation result
|
||||||
|
*/
|
||||||
|
validateMarkdown(markdownText) {
|
||||||
|
try {
|
||||||
|
const html = this.renderToHtml(markdownText);
|
||||||
|
return {
|
||||||
|
valid: true,
|
||||||
|
html,
|
||||||
|
warnings: this.getMarkdownWarnings(markdownText)
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Invalid markdown format',
|
||||||
|
error: error.message
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get warnings for markdown content
|
||||||
|
* @param {string} markdownText - Markdown to check
|
||||||
|
* @returns {Array} Array of warnings
|
||||||
|
*/
|
||||||
|
getMarkdownWarnings(markdownText) {
|
||||||
|
const warnings = [];
|
||||||
|
|
||||||
|
// Check for potentially problematic patterns
|
||||||
|
if (markdownText.includes('<script')) {
|
||||||
|
warnings.push('Script tags detected in markdown');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (markdownText.includes('javascript:')) {
|
||||||
|
warnings.push('JavaScript URLs detected in markdown');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for excessive nesting
|
||||||
|
const headerCount = (markdownText.match(/^#+\s/gm) || []).length;
|
||||||
|
if (headerCount > 10) {
|
||||||
|
warnings.push('Many headers detected - consider simplifying structure');
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strip markdown formatting to plain text
|
||||||
|
* @param {string} markdownText - Markdown content
|
||||||
|
* @returns {string} Plain text
|
||||||
|
*/
|
||||||
|
toPlainText(markdownText) {
|
||||||
|
return markdownText
|
||||||
|
.replace(/#{1,6}\s+/g, '') // Remove headers
|
||||||
|
.replace(/\*\*(.*?)\*\*/g, '$1') // Remove bold
|
||||||
|
.replace(/\*(.*?)\*/g, '$1') // Remove italic
|
||||||
|
.replace(/\[(.*?)\]\(.*?\)/g, '$1') // Remove links, keep text
|
||||||
|
.replace(/`(.*?)`/g, '$1') // Remove code
|
||||||
|
.replace(/^\s*[-*+]\s+/gm, '') // Remove list markers
|
||||||
|
.replace(/^\s*\d+\.\s+/gm, '') // Remove numbered list markers
|
||||||
|
.replace(/\n\n+/g, '\n\n') // Normalize whitespace
|
||||||
|
.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for module usage
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = InsertrMarkdownProcessor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global export for browser usage
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.InsertrMarkdownProcessor = InsertrMarkdownProcessor;
|
||||||
|
}
|
||||||
194
demo-site/insertr/validation.js
Normal file
194
demo-site/insertr/validation.js
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
/**
|
||||||
|
* Insertr Validation Module
|
||||||
|
* Client-side validation for user experience (not security)
|
||||||
|
*/
|
||||||
|
|
||||||
|
class InsertrValidation {
|
||||||
|
constructor(config, domPurify = null) {
|
||||||
|
this.config = config;
|
||||||
|
this.DOMPurify = domPurify;
|
||||||
|
this.limits = config.getValidationLimits();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate input based on field type
|
||||||
|
* @param {string} input - Input to validate
|
||||||
|
* @param {string} fieldType - Type of field
|
||||||
|
* @returns {Object} Validation result
|
||||||
|
*/
|
||||||
|
validateInput(input, fieldType) {
|
||||||
|
if (!input || typeof input !== 'string') {
|
||||||
|
return { valid: false, message: 'Content cannot be empty' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Basic length validation
|
||||||
|
if (input.length > this.limits.maxContentLength) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: `Content is too long (max ${this.limits.maxContentLength.toLocaleString()} characters)`
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Field-specific validation
|
||||||
|
switch (fieldType) {
|
||||||
|
case 'text':
|
||||||
|
return this.validateTextInput(input);
|
||||||
|
case 'textarea':
|
||||||
|
return this.validateTextInput(input);
|
||||||
|
case 'link':
|
||||||
|
return this.validateLinkInput(input);
|
||||||
|
case 'markdown':
|
||||||
|
return this.validateMarkdownInput(input);
|
||||||
|
default:
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate plain text input
|
||||||
|
* @param {string} input - Text to validate
|
||||||
|
* @returns {Object} Validation result
|
||||||
|
*/
|
||||||
|
validateTextInput(input) {
|
||||||
|
// Check for obvious HTML that users might accidentally include
|
||||||
|
if (input.includes('<script>') || input.includes('</script>')) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Script tags are not allowed for security reasons'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.includes('<') && input.includes('>')) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'HTML tags are not allowed in text fields. Use markdown collections for formatted content.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate link/URL input
|
||||||
|
* @param {string} input - URL to validate
|
||||||
|
* @returns {Object} Validation result
|
||||||
|
*/
|
||||||
|
validateLinkInput(input) {
|
||||||
|
// Basic URL validation for user feedback
|
||||||
|
const urlPattern = /^(https?:\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\/\w \.-]*)*\/?$/;
|
||||||
|
if (input.startsWith('http') && !urlPattern.test(input)) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Please enter a valid URL (e.g., https://example.com)'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate markdown input
|
||||||
|
* @param {string} input - Markdown to validate
|
||||||
|
* @returns {Object} Validation result
|
||||||
|
*/
|
||||||
|
validateMarkdownInput(input) {
|
||||||
|
// Check for potentially problematic content
|
||||||
|
if (input.includes('<script>') || input.includes('javascript:')) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Script content is not allowed for security reasons'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Warn about excessive HTML (user might be pasting from Word/etc)
|
||||||
|
const htmlTagCount = (input.match(/<[^>]+>/g) || []).length;
|
||||||
|
if (htmlTagCount > this.limits.maxHtmlTags) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
message: 'Too much HTML detected. Please use markdown formatting instead of HTML tags.'
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show validation message to user
|
||||||
|
* @param {HTMLElement} element - Element to show message near
|
||||||
|
* @param {string} message - Message to show
|
||||||
|
* @param {boolean} isError - Whether this is an error message
|
||||||
|
*/
|
||||||
|
showValidationMessage(element, message, isError = true) {
|
||||||
|
// Remove existing message
|
||||||
|
const existingMsg = element.parentNode.querySelector('.insertr-validation-message');
|
||||||
|
if (existingMsg) {
|
||||||
|
existingMsg.remove();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!message) return;
|
||||||
|
|
||||||
|
// Create new message
|
||||||
|
const msgElement = document.createElement('div');
|
||||||
|
msgElement.className = `insertr-validation-message ${isError ? 'error' : 'success'}`;
|
||||||
|
msgElement.textContent = message;
|
||||||
|
|
||||||
|
// Insert after the element
|
||||||
|
element.parentNode.insertBefore(msgElement, element.nextSibling);
|
||||||
|
|
||||||
|
// Auto-remove after delay
|
||||||
|
if (!isError) {
|
||||||
|
setTimeout(() => {
|
||||||
|
if (msgElement.parentNode) {
|
||||||
|
msgElement.remove();
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize content for display (basic client-side sanitization)
|
||||||
|
* @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':
|
||||||
|
// Basic URL sanitization
|
||||||
|
if (content.startsWith('javascript:') || content.startsWith('data:')) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
case 'markdown':
|
||||||
|
// For markdown, we'll let marked.js handle it with our safe config
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export for module usage
|
||||||
|
if (typeof module !== 'undefined' && module.exports) {
|
||||||
|
module.exports = InsertrValidation;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Global export for browser usage
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
window.InsertrValidation = InsertrValidation;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user