- Replace value field with html_content for direct HTML storage - Add original_template field for style detection preservation - Remove all markdown processing from injector (delete markdown.go) - Fix critical content extraction/injection bugs in engine - Add missing UpdateContent PUT handler for content persistence - Fix API client field names and add updateContent() method - Resolve content type validation (only allow text/link types) - Add UUID-based ID generation to prevent collisions - Complete first-pass processing workflow for unprocessed elements - Verify end-to-end: Enhancement → Database → API → Editor → Persistence All 37 files updated for HTML-first content management system. Phase 3a implementation complete and production ready.
167 lines
5.9 KiB
JavaScript
167 lines
5.9 KiB
JavaScript
/**
|
|
* InsertrCore - Core functionality for content management
|
|
*/
|
|
export class InsertrCore {
|
|
constructor(options = {}) {
|
|
this.options = {
|
|
apiEndpoint: options.apiEndpoint || '/api/content',
|
|
siteId: options.siteId || 'default',
|
|
...options
|
|
};
|
|
}
|
|
|
|
// Find all enhanced elements on the page with container expansion
|
|
findEnhancedElements() {
|
|
const directElements = document.querySelectorAll('.insertr');
|
|
const expandedElements = [];
|
|
|
|
directElements.forEach(element => {
|
|
if (this.isContainer(element) && !element.classList.contains('insertr-group')) {
|
|
// Container element (.insertr) - expand to viable children
|
|
const children = this.findViableChildren(element);
|
|
expandedElements.push(...children);
|
|
} else {
|
|
// Regular element or group (.insertr-group)
|
|
expandedElements.push(element);
|
|
}
|
|
});
|
|
|
|
return expandedElements;
|
|
}
|
|
|
|
// Check if element is a container that should expand to children
|
|
isContainer(element) {
|
|
const containerTags = new Set([
|
|
'div', 'section', 'article', 'header',
|
|
'footer', 'main', 'aside', 'nav'
|
|
]);
|
|
|
|
return containerTags.has(element.tagName.toLowerCase());
|
|
}
|
|
|
|
// Find viable children for editing (elements with only text content)
|
|
findViableChildren(containerElement) {
|
|
const viable = [];
|
|
|
|
for (const child of containerElement.children) {
|
|
// Skip elements that already have .insertr class
|
|
if (child.classList.contains('insertr')) {
|
|
continue;
|
|
}
|
|
|
|
// Skip self-closing elements
|
|
if (this.isSelfClosing(child)) {
|
|
continue;
|
|
}
|
|
|
|
// Check if element has only text content (no nested HTML elements)
|
|
if (this.hasOnlyTextContent(child)) {
|
|
viable.push(child);
|
|
}
|
|
}
|
|
|
|
return viable;
|
|
}
|
|
|
|
// Check if element is viable for editing (allows simple formatting)
|
|
hasOnlyTextContent(element) {
|
|
// Allow elements with simple formatting tags
|
|
const allowedTags = new Set(['strong', 'b', 'em', 'i', 'a', 'span', 'code']);
|
|
|
|
for (const child of element.children) {
|
|
const tagName = child.tagName.toLowerCase();
|
|
|
|
// If child is not an allowed formatting tag, reject
|
|
if (!allowedTags.has(tagName)) {
|
|
return false;
|
|
}
|
|
|
|
// If formatting tag has nested complex elements, reject
|
|
if (child.children.length > 0) {
|
|
// Recursively check nested content isn't too complex
|
|
for (const nestedChild of child.children) {
|
|
const nestedTag = nestedChild.tagName.toLowerCase();
|
|
if (!allowedTags.has(nestedTag)) {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Element has only text and/or simple formatting - this is viable
|
|
return element.textContent.trim().length > 0;
|
|
}
|
|
|
|
// Check if element is self-closing
|
|
isSelfClosing(element) {
|
|
const selfClosingTags = new Set([
|
|
'img', 'input', 'br', 'hr', 'meta', 'link',
|
|
'area', 'base', 'col', 'embed', 'source', 'track', 'wbr'
|
|
]);
|
|
|
|
return selfClosingTags.has(element.tagName.toLowerCase());
|
|
}
|
|
|
|
// Get element metadata
|
|
getElementMetadata(element) {
|
|
const existingId = element.getAttribute('data-content-id');
|
|
|
|
// Ensure element has insertr class for server processing
|
|
if (!element.classList.contains('insertr')) {
|
|
element.classList.add('insertr');
|
|
}
|
|
|
|
// Send HTML markup to server for unified ID generation
|
|
return {
|
|
contentId: existingId, // null if new content, existing ID if updating
|
|
contentType: element.getAttribute('data-content-type') || this.detectContentType(element),
|
|
element: element,
|
|
htmlMarkup: element.outerHTML // Server will generate ID from this
|
|
};
|
|
}
|
|
|
|
// Get current file path from URL for consistent ID generation
|
|
getCurrentFilePath() {
|
|
const path = window.location.pathname;
|
|
if (path === '/' || path === '') {
|
|
return 'index.html';
|
|
}
|
|
// Remove leading slash: "/about.html" → "about.html"
|
|
return path.replace(/^\//, '');
|
|
}
|
|
|
|
// Detect content type for elements without data-content-type
|
|
detectContentType(element) {
|
|
const tag = element.tagName.toLowerCase();
|
|
|
|
// Only return database-valid types: 'text' or 'link'
|
|
if (tag === 'a' || tag === 'button') {
|
|
return 'link';
|
|
}
|
|
|
|
// All other elements are text content
|
|
return 'text';
|
|
}
|
|
|
|
// Get all elements with their metadata, including group elements
|
|
getAllElements() {
|
|
const directElements = document.querySelectorAll('.insertr, .insertr-group');
|
|
const processedElements = [];
|
|
|
|
directElements.forEach(element => {
|
|
if (element.classList.contains('insertr-group')) {
|
|
// Group element - treat as single editable unit
|
|
processedElements.push(element);
|
|
} else if (this.isContainer(element)) {
|
|
// Container element - expand to children
|
|
const children = this.findViableChildren(element);
|
|
processedElements.push(...children);
|
|
} else {
|
|
// Regular element
|
|
processedElements.push(element);
|
|
}
|
|
});
|
|
|
|
return Array.from(processedElements).map(el => this.getElementMetadata(el));
|
|
}
|
|
} |