refactor: Complete UI cleanup and simplify editor architecture

- Remove complex style preservation system from editor
- Simplify markdown conversion back to straightforward approach
- Remove StyleContext class and style-aware conversion methods
- Switch content type from 'html' back to 'markdown' for consistency
- Clean up editor workflow to focus on core markdown editing
- Remove ~500 lines of unnecessary style complexity

This completes the UI unification cleanup by removing the overly complex
style preservation system that was making the editor harder to maintain.
This commit is contained in:
2025-09-19 16:15:56 +02:00
parent b7998a4b3c
commit 968e64a57e
6 changed files with 21 additions and 496 deletions

View File

@@ -299,13 +299,13 @@ func (h *ContentHandler) CreateContent(w http.ResponseWriter, r *http.Request) {
} }
} }
// Determine content type: use provided type, fallback to existing type, default to "html" // Determine content type: use provided type, fallback to existing type, default to "text"
contentType := req.Type contentType := req.Type
if contentType == "" && contentExists { if contentType == "" && contentExists {
contentType = h.getContentType(existingContent) contentType = h.getContentType(existingContent)
} }
if contentType == "" { if contentType == "" {
contentType = "html" // default type for new content (changed from "text") contentType = "text" // default type for new content
} }
var content interface{} var content interface{}

View File

@@ -103,8 +103,6 @@ func (i *Injector) InjectBulkContent(elements []ElementWithID) error {
i.injectTextContent(elem.Element.Node, contentItem.Value) i.injectTextContent(elem.Element.Node, contentItem.Value)
case "markdown": case "markdown":
i.injectMarkdownContent(elem.Element.Node, contentItem.Value) i.injectMarkdownContent(elem.Element.Node, contentItem.Value)
case "html":
i.injectHTMLContent(elem.Element.Node, contentItem.Value)
case "link": case "link":
i.injectLinkContent(elem.Element.Node, contentItem.Value) i.injectLinkContent(elem.Element.Node, contentItem.Value)
default: default:

View File

@@ -1,5 +1,4 @@
import { InsertrFormRenderer } from '../ui/form-renderer.js'; import { InsertrFormRenderer } from '../ui/form-renderer.js';
import { markdownConverter } from '../utils/markdown.js';
/** /**
* InsertrEditor - Content editing workflow and business logic * InsertrEditor - Content editing workflow and business logic
@@ -115,35 +114,16 @@ export class InsertrEditor {
try { try {
// Extract content value based on type // Extract content value based on type
let markdownContent; let contentValue;
if (meta.element.tagName.toLowerCase() === 'a') { if (meta.element.tagName.toLowerCase() === 'a') {
// For links, save the text content (URL is handled separately if needed) // For links, save the text content (URL is handled separately if needed)
markdownContent = formData.text || formData; contentValue = formData.text || formData;
} else { } else {
markdownContent = formData.text || formData; contentValue = formData.text || formData;
}
// Convert markdown to HTML with style preservation
let contentValue;
const contentType = this.determineContentType(meta.element);
if (contentType === 'html') {
// Extract style context from original element and convert markdown to HTML
const { markdown, styleContext } = markdownConverter.htmlToMarkdownWithContext(meta.element.innerHTML, meta.element);
if (styleContext && styleContext.hasPreservableContent) {
// Convert markdown back to HTML with style preservation
contentValue = markdownConverter.markdownToHtmlWithStyles(markdownContent, styleContext);
} else {
// No styles to preserve, simple conversion
contentValue = markdownConverter.markdownToHtml(markdownContent);
}
} else {
// For other content types (text, link), use markdown as-is
contentValue = markdownContent;
} }
// Universal upsert - server handles ID extraction/generation from markup // Universal upsert - server handles ID extraction/generation from markup
const contentType = this.determineContentType(meta.element);
const result = await this.apiClient.createContent( const result = await this.apiClient.createContent(
contentValue, contentValue,
contentType, contentType,
@@ -175,8 +155,8 @@ export class InsertrEditor {
return 'link'; return 'link';
} }
// ALL text elements use HTML storage with markdown editing interface // ALL text elements use markdown for consistent editing experience
return 'html'; return 'markdown';
} }
handleCancel(meta) { handleCancel(meta) {

View File

@@ -375,11 +375,10 @@ class EditContext {
this.primaryElement = elements[0]; this.primaryElement = elements[0];
this.originalContent = null; this.originalContent = null;
this.currentContent = currentContent; this.currentContent = currentContent;
this.styleContext = null; // Store style context for preservation
} }
/** /**
* Extract content from elements in markdown format with style preservation * Extract content from elements in markdown format
*/ */
extractContent() { extractContent() {
if (this.elements.length === 1) { if (this.elements.length === 1) {
@@ -387,30 +386,22 @@ class EditContext {
// Handle links specially // Handle links specially
if (element.tagName.toLowerCase() === 'a') { if (element.tagName.toLowerCase() === 'a') {
// Extract with style context for links
const { markdown, styleContext } = markdownConverter.htmlToMarkdownWithContext(element.innerHTML, element);
this.styleContext = styleContext;
return { return {
text: markdown, text: markdownConverter.htmlToMarkdown(element.innerHTML),
url: element.href url: element.href
}; };
} }
// Single element - convert to markdown with style context // Single element - convert to markdown
const { markdown, styleContext } = markdownConverter.htmlToMarkdownWithContext(element.innerHTML, element); return markdownConverter.htmlToMarkdown(element.innerHTML);
this.styleContext = styleContext;
return markdown;
} else { } else {
// Multiple elements - use group extraction with style context // Multiple elements - use group extraction
const { markdown, styleContext } = markdownConverter.extractGroupMarkdownWithContext(this.elements); return markdownConverter.extractGroupMarkdown(this.elements);
this.styleContext = styleContext;
return markdown;
} }
} }
/** /**
* Apply content to elements from markdown/object with style preservation * Apply content to elements from markdown/object
*/ */
applyContent(content) { applyContent(content) {
if (this.elements.length === 1) { if (this.elements.length === 1) {
@@ -418,24 +409,19 @@ class EditContext {
// Handle links specially // Handle links specially
if (element.tagName.toLowerCase() === 'a' && typeof content === 'object') { if (element.tagName.toLowerCase() === 'a' && typeof content === 'object') {
const html = this.styleContext ? element.innerHTML = markdownConverter.markdownToHtml(content.text || '');
markdownConverter.markdownToHtmlWithStyles(content.text || '', this.styleContext) :
markdownConverter.markdownToHtml(content.text || '');
element.innerHTML = html;
if (content.url) { if (content.url) {
element.href = content.url; element.href = content.url;
} }
return; return;
} }
// Single element - convert markdown to HTML with style restoration // Single element - convert markdown to HTML
const html = this.styleContext ? const html = markdownConverter.markdownToHtml(content);
markdownConverter.markdownToHtmlWithStyles(content, this.styleContext) :
markdownConverter.markdownToHtml(content);
element.innerHTML = html; element.innerHTML = html;
} else { } else {
// Multiple elements - use group update with style preservation // Multiple elements - use group update
markdownConverter.updateGroupElementsWithStyles(this.elements, content, this.styleContext); markdownConverter.updateGroupElements(this.elements, content);
} }
} }

View File

@@ -1,9 +1,8 @@
/** /**
* Markdown conversion utilities using Marked and Turndown with Style Preservation * Markdown conversion utilities using Marked and Turndown
*/ */
import { marked } from 'marked'; import { marked } from 'marked';
import TurndownService from 'turndown'; import TurndownService from 'turndown';
import { StyleContext } from './style-context.js';
/** /**
* MarkdownConverter - Handles bidirectional HTML ↔ Markdown conversion * MarkdownConverter - Handles bidirectional HTML ↔ Markdown conversion
@@ -12,7 +11,6 @@ export class MarkdownConverter {
constructor() { constructor() {
this.initializeMarked(); this.initializeMarked();
this.initializeTurndown(); this.initializeTurndown();
this.styleContext = new StyleContext();
} }
/** /**
@@ -188,37 +186,6 @@ export class MarkdownConverter {
} }
} }
/**
* Convert HTML to Markdown with style context preservation
* @param {string} html - HTML string to convert
* @param {HTMLElement} originalElement - Original DOM element for context
* @returns {Object} - Object containing markdown and style context
*/
htmlToMarkdownWithContext(html, originalElement = null) {
if (!html || html.trim() === '') {
return { markdown: '', styleContext: null };
}
let styleContext = null;
// Extract style context if original element provided
if (originalElement) {
styleContext = this.styleContext.extractStyleContext(originalElement);
} else {
// Create temporary element to analyze
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
styleContext = this.styleContext.extractStyleContext(tempDiv);
}
const markdown = this.htmlToMarkdown(html);
return {
markdown,
styleContext: styleContext.hasPreservableContent ? styleContext : null
};
}
/** /**
* Convert Markdown to HTML * Convert Markdown to HTML
* @param {string} markdown - Markdown string to convert * @param {string} markdown - Markdown string to convert
@@ -243,51 +210,6 @@ export class MarkdownConverter {
} }
} }
/**
* Convert Markdown to HTML with style context restoration
* @param {string} markdown - Markdown string to convert
* @param {Object} styleContext - Style context to restore
* @returns {string} - HTML string with styles restored
*/
markdownToHtmlWithStyles(markdown, styleContext) {
if (!markdown || markdown.trim() === '') {
return '';
}
// Convert markdown to basic HTML first
const basicHtml = this.markdownToHtml(markdown);
// If no style context, return basic HTML
if (!styleContext || !this.styleContext.validateContext(styleContext)) {
return basicHtml;
}
// Apply style context to the converted HTML
return this.applyStyleContextToHtml(basicHtml, styleContext);
}
/**
* Apply style context to HTML string
* @param {string} html - HTML string to enhance
* @param {Object} styleContext - Style context to apply
* @returns {string} - Enhanced HTML with styles applied
*/
applyStyleContextToHtml(html, styleContext) {
try {
// Create temporary container
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// Apply style context
this.styleContext.applyStyleContext(tempDiv, styleContext);
return tempDiv.innerHTML;
} catch (error) {
console.warn('Failed to apply style context:', error);
return html; // Return original HTML on error
}
}
/** /**
* Extract HTML content from a group of elements * Extract HTML content from a group of elements
* @param {HTMLElement[]} elements - Array of DOM elements * @param {HTMLElement[]} elements - Array of DOM elements
@@ -313,73 +235,6 @@ export class MarkdownConverter {
return htmlParts.join('\n'); return htmlParts.join('\n');
} }
/**
* Extract HTML content with style context from a group of elements
* @param {HTMLElement[]} elements - Array of DOM elements
* @returns {Object} - Object with HTML content and combined style context
*/
extractGroupHTMLWithContext(elements) {
const htmlParts = [];
const allStyleContexts = [];
elements.forEach((element, index) => {
// Extract style context for this element
const elementContext = this.styleContext.extractStyleContext(element);
if (elementContext.hasPreservableContent) {
allStyleContexts.push({
index,
context: elementContext
});
}
// Extract HTML content
const html = element.innerHTML.trim();
if (html) {
if (element.tagName.toLowerCase() === 'p') {
htmlParts.push(element.outerHTML);
} else {
htmlParts.push(`<p>${html}</p>`);
}
}
});
// Combine all style contexts
const combinedContext = this.combineStyleContexts(allStyleContexts);
return {
html: htmlParts.join('\n'),
styleContext: combinedContext
};
}
/**
* Combine multiple style contexts into a single context
* @param {Array} styleContexts - Array of style contexts with index info
* @returns {Object} - Combined style context
*/
combineStyleContexts(styleContexts) {
if (styleContexts.length === 0) {
return null;
}
const combinedMap = new Map();
let hasContent = false;
styleContexts.forEach(({ index, context }) => {
// Adjust paths to include element index
for (const [path, elementInfo] of context.elementMap) {
const adjustedPath = `${index}.${path}`;
combinedMap.set(adjustedPath, elementInfo);
hasContent = true;
}
});
return hasContent ? {
elementMap: combinedMap,
hasPreservableContent: true
} : null;
}
/** /**
* Convert HTML content from group elements to markdown * Convert HTML content from group elements to markdown
* @param {HTMLElement[]} elements - Array of DOM elements * @param {HTMLElement[]} elements - Array of DOM elements
@@ -391,21 +246,6 @@ export class MarkdownConverter {
return markdown; return markdown;
} }
/**
* Convert HTML content from group elements to markdown with style context
* @param {HTMLElement[]} elements - Array of DOM elements
* @returns {Object} - Object with markdown and style context
*/
extractGroupMarkdownWithContext(elements) {
const { html, styleContext } = this.extractGroupHTMLWithContext(elements);
const markdown = this.htmlToMarkdown(html);
return {
markdown,
styleContext
};
}
/** /**
* Update group elements with markdown content * Update group elements with markdown content
* @param {HTMLElement[]} elements - Array of DOM elements to update * @param {HTMLElement[]} elements - Array of DOM elements to update
@@ -442,47 +282,6 @@ export class MarkdownConverter {
} }
} }
} }
/**
* Update group elements with markdown content and style context
* @param {HTMLElement[]} elements - Array of DOM elements to update
* @param {string} markdown - Markdown content to render
* @param {Object} styleContext - Style context to apply
*/
updateGroupElementsWithStyles(elements, markdown, styleContext) {
// Convert markdown to HTML with styles
const html = styleContext ?
this.markdownToHtmlWithStyles(markdown, styleContext) :
this.markdownToHtml(markdown);
// Split HTML into paragraphs
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
const paragraphs = Array.from(tempDiv.querySelectorAll('p, div, h1, h2, h3, h4, h5, h6'));
// Handle case where we have more/fewer paragraphs than elements
const maxCount = Math.max(elements.length, paragraphs.length);
for (let i = 0; i < maxCount; i++) {
if (i < elements.length && i < paragraphs.length) {
// Update existing element with corresponding paragraph
elements[i].innerHTML = paragraphs[i].innerHTML;
} else if (i < elements.length) {
// More elements than paragraphs - clear extra elements
elements[i].innerHTML = '';
} else if (i < paragraphs.length) {
// More paragraphs than elements - create new element
const newElement = document.createElement('p');
newElement.innerHTML = paragraphs[i].innerHTML;
// Insert after the last existing element
const lastElement = elements[elements.length - 1];
lastElement.parentNode.insertBefore(newElement, lastElement.nextSibling);
elements.push(newElement); // Add to our elements array for future updates
}
}
}
} }
// Export singleton instance // Export singleton instance

View File

@@ -1,238 +0,0 @@
/**
* Style Context Extraction System for Insertr
*
* Analyzes HTML elements to extract styling context for preservation
* during markdown editing. Focuses on attributes, classes, and inline styles
* that should be preserved when content is converted to/from markdown.
*/
export class StyleContext {
constructor() {
this.preservedAttributes = new Set([
'class', 'id', 'rel', 'target', 'title', 'alt', 'href',
'src', 'data-*', 'aria-*', 'role', 'tabindex'
]);
}
/**
* Extract complete style context from an HTML element
* @param {HTMLElement} element - The element to analyze
* @returns {Object} Style context with element map and metadata
*/
extractStyleContext(element) {
const context = {
elementMap: new Map(),
rootElement: this.cloneElementStructure(element),
hasPreservableContent: false
};
this.analyzeElement(element, context, []);
return context;
}
/**
* Recursively analyze element and its children for style preservation
* @param {HTMLElement} element - Current element
* @param {Object} context - Style context being built
* @param {Array} path - Path to current element
*/
analyzeElement(element, context, path) {
const elementInfo = this.extractElementInfo(element);
if (elementInfo.hasPreservableAttributes) {
context.hasPreservableContent = true;
context.elementMap.set(path.join('.'), elementInfo);
}
// Analyze children one level deep for now
Array.from(element.children).forEach((child, index) => {
const childPath = [...path, index.toString()];
this.analyzeElement(child, context, childPath);
});
}
/**
* Extract styling information from a single element
* @param {HTMLElement} element - Element to analyze
* @returns {Object} Element style information
*/
extractElementInfo(element) {
const tagName = element.tagName.toLowerCase();
const attributes = this.extractAttributes(element);
const hasPreservableAttributes = Object.keys(attributes).length > 0;
return {
tagName,
attributes,
hasPreservableAttributes,
textContent: this.getDirectTextContent(element),
hasChildren: element.children.length > 0
};
}
/**
* Extract relevant attributes from an element
* @param {HTMLElement} element - Element to extract attributes from
* @returns {Object} Filtered attributes object
*/
extractAttributes(element) {
const attributes = {};
for (const attr of element.attributes) {
const name = attr.name.toLowerCase();
// Include if it's in our preserved set or matches a pattern
if (this.shouldPreserveAttribute(name)) {
attributes[name] = attr.value;
}
}
return attributes;
}
/**
* Check if an attribute should be preserved
* @param {string} attributeName - Name of the attribute
* @returns {boolean} Whether to preserve this attribute
*/
shouldPreserveAttribute(attributeName) {
// Direct matches
if (this.preservedAttributes.has(attributeName)) {
return true;
}
// Pattern matches (data-*, aria-*)
return attributeName.startsWith('data-') ||
attributeName.startsWith('aria-');
}
/**
* Get only the direct text content of an element (not from children)
* @param {HTMLElement} element - Element to get text from
* @returns {string} Direct text content
*/
getDirectTextContent(element) {
let text = '';
for (const node of element.childNodes) {
if (node.nodeType === Node.TEXT_NODE) {
text += node.textContent;
}
}
return text.trim();
}
/**
* Create a structural clone of an element (attributes only, no content)
* @param {HTMLElement} element - Element to clone structure of
* @returns {Object} Cloned structure
*/
cloneElementStructure(element) {
return {
tagName: element.tagName.toLowerCase(),
attributes: this.extractAttributes(element),
children: Array.from(element.children).map(child =>
this.cloneElementStructure(child)
)
};
}
/**
* Apply style context back to an HTML element
* @param {HTMLElement} element - Element to apply styles to
* @param {Object} context - Style context to apply
* @param {Array} path - Current path in the element tree
*/
applyStyleContext(element, context, path = []) {
const pathKey = path.join('.');
const elementInfo = context.elementMap.get(pathKey);
if (elementInfo) {
this.applyAttributes(element, elementInfo.attributes);
}
// Apply to children
Array.from(element.children).forEach((child, index) => {
const childPath = [...path, index.toString()];
this.applyStyleContext(child, context, childPath);
});
}
/**
* Apply attributes to an element
* @param {HTMLElement} element - Element to apply attributes to
* @param {Object} attributes - Attributes to apply
*/
applyAttributes(element, attributes) {
for (const [name, value] of Object.entries(attributes)) {
element.setAttribute(name, value);
}
}
/**
* Generate markdown formatting options based on detected styles
* @param {Object} context - Style context
* @returns {Object} Formatting options for markdown conversion
*/
generateFormattingOptions(context) {
const options = {
preserveLinks: true,
preserveStrong: true,
preserveEmphasis: true,
customElements: new Map()
};
// Analyze element map to detect patterns
for (const [path, elementInfo] of context.elementMap) {
if (elementInfo.tagName === 'a' && elementInfo.attributes.class) {
options.customElements.set('link', {
tagName: 'a',
attributes: elementInfo.attributes
});
}
if (elementInfo.tagName === 'strong' && elementInfo.attributes.class) {
options.customElements.set('strong', {
tagName: 'strong',
attributes: elementInfo.attributes
});
}
if (elementInfo.tagName === 'span' && elementInfo.attributes.class) {
options.customElements.set('span', {
tagName: 'span',
attributes: elementInfo.attributes
});
}
}
return options;
}
/**
* Validate that style context can be safely applied
* @param {Object} context - Style context to validate
* @returns {boolean} Whether context is valid and safe
*/
validateContext(context) {
if (!context || !context.elementMap) {
return false;
}
// Check for potentially dangerous attributes
for (const [path, elementInfo] of context.elementMap) {
for (const [attr, value] of Object.entries(elementInfo.attributes)) {
// Block script-related attributes for security
if (attr.toLowerCase().startsWith('on') ||
attr.toLowerCase() === 'javascript' ||
(typeof value === 'string' && value.includes('javascript:'))) {
console.warn(`Blocking potentially dangerous attribute: ${attr}="${value}"`);
return false;
}
}
}
return true;
}
}
export default StyleContext;