feat: implement Phase 3 container transformation with CLASSES.md compliance

- Add backend container transformation in engine.go following syntactic sugar specification
- Containers with .insertr get class removed and viable children get .insertr added
- Remove incorrect frontend container expansion - frontend only finds enhanced elements
- Fix StyleAwareEditor hasMultiPropertyElements runtime error
- Add addClass/removeClass methods to ContentEngine for class manipulation
- Update frontend to match HTML-first approach with no runtime container logic
- Test verified: container <section class='insertr'> transforms to individual h1.insertr, p.insertr, button.insertr

This completes the container expansion functionality per CLASSES.md:
Developer convenience (one .insertr enables section editing) + granular control (individual element editing)
This commit is contained in:
2025-09-21 19:17:12 +02:00
parent 4ef032cad6
commit b5e601d09f
16 changed files with 568 additions and 1407 deletions

View File

@@ -1,361 +0,0 @@
# Server Update Plan - Phase 3a: HTML-First API Architecture
## 🎯 Current Status & Vision
### **What We Discovered**
Our frontend has evolved to a sophisticated **HTML-first approach** with:
- StyleAware editor with automatic style detection from nested elements
- HTML preservation with perfect attribute fidelity
- Rich content editing capabilities with formatting toolbar
- Template-based style preservation using CLASSES.md methodology
However, our **server API is still text-focused**, creating a fundamental mismatch between frontend capabilities and backend storage.
### **Core Requirements Identified**
1. **HTML-First Storage**: Replace `value` with `html_content` field for direct HTML storage
2. **Template Preservation**: Store `original_template` for consistent style detection
3. **Enhancer-First Workflow**: Enhancer stores content on first pass, ignores processed elements
4. **No Markdown Processing**: Remove all markdown logic from injector - HTML only
5. **StyleAware Editor Compatibility**: API must match library expectations
6. **Dev Convenience**: Option to clean DB for fresh development iterations
## 🏗️ Implementation Strategy
### **1. HTML-First Database Schema (Direct Replacement)**
**Updated Schema:**
```sql
-- SQLite schema
CREATE TABLE content (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
html_content TEXT NOT NULL, -- Rich HTML content (innerHTML)
original_template TEXT, -- Original element markup for style detection (outerHTML)
type TEXT NOT NULL,
created_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
updated_at INTEGER DEFAULT (strftime('%s', 'now')) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (id, site_id)
);
-- PostgreSQL schema
CREATE TABLE content (
id TEXT NOT NULL,
site_id TEXT NOT NULL,
html_content TEXT NOT NULL, -- Rich HTML content (innerHTML)
original_template TEXT, -- Original element markup for style detection (outerHTML)
type TEXT NOT NULL,
created_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
updated_at BIGINT DEFAULT EXTRACT(EPOCH FROM NOW()) NOT NULL,
last_edited_by TEXT DEFAULT 'system' NOT NULL,
PRIMARY KEY (id, site_id)
);
```
**Key Changes:**
-**Removed `value` field** - HTML serves both editing and injection needs
-**Added `html_content`** - Direct HTML storage for content editing and injection
-**Added `original_template`** - Preserves developer templates for StyleAware editor style detection
-**Simplified approach** - No complex template locking, focus on core functionality
### **2. Enhancer-First Workflow (First-Pass Processing)**
#### **Unprocessed Element Detection:**
- Elements without `data-content-id` attribute are unprocessed
- Enhancer processes these elements and assigns IDs
- Subsequent enhancer runs skip elements that already have `data-content-id`
#### **Content Storage on First Pass:**
```go
func processElement(node *html.Node, siteID string) {
// Check if already processed
existingID := getAttribute(node, "data-content-id")
if existingID != "" {
return // Skip - already processed
}
// Extract content and template
contentID := generateContentID(node, filePath)
htmlContent := extractInnerHTML(node) // For editing/injection
originalTemplate := extractOuterHTML(node) // For style detection
contentType := determineContentType(node)
// Store in database
createContent(siteID, contentID, htmlContent, originalTemplate, contentType)
// Mark as processed
setAttribute(node, "data-content-id", contentID)
setAttribute(node, "data-content-type", contentType)
}
```
#### **Development Convenience:**
- First-pass processing automatically handles new elements without affecting existing content
- Developers can iterate on unprocessed parts while preserving existing content
### **3. Injector Redesign (HTML-Only)**
#### **Remove Markdown Processing:**
- Delete `MarkdownProcessor` and all markdown-related logic
- Direct HTML injection using existing `injectHTMLContent()` method
- Simplified injection flow focused on HTML fidelity
#### **Updated Injection Process:**
```go
func (i *Injector) InjectContent(element *Element, contentID string) error {
// Fetch content from database
contentItem, err := i.client.GetContent(i.siteID, contentID)
if err != nil || contentItem == nil {
// No content found - add attributes but keep original content
i.AddContentAttributes(element.Node, contentID, element.Type)
return nil
}
// Direct HTML injection - no markdown processing
i.injectHTMLContent(element.Node, contentItem.HTMLContent)
i.AddContentAttributes(element.Node, contentID, element.Type)
return nil
}
```
#### **Content Type Handling:**
- All content types use HTML storage and injection
- Remove type-specific processing (text, markdown, link)
- StyleAware editor handles rich editing based on element context
### **4. StyleAware Editor Integration**
#### **API Response Format (Matches Editor Expectations):**
```json
{
"id": "hero-title-abc123",
"site_id": "mysite",
"html_content": "<h1>Welcome to <em>Our</em> Company</h1>",
"original_template": "<h1 class=\"insertr brand-heading\">Welcome to <span class=\"brand\">Our Company</span></h1>",
"type": "text",
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T10:30:00Z",
"last_edited_by": "user@example.com"
}
```
#### **Editor Integration:**
- **Style Detection**: Uses `original_template` for consistent formatting options
- **Content Editing**: Uses `html_content` for rich text editing
- **Perfect Alignment**: Response format matches StyleAware editor analysis requirements
- **Multi-Property Support**: Complex elements (links) work seamlessly with preserved templates
#### **Updated API Models:**
```go
type ContentItem struct {
ID string `json:"id"`
SiteID string `json:"site_id"`
HTMLContent string `json:"html_content"` // For editor content
OriginalTemplate string `json:"original_template"` // For style detection
Type string `json:"type"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
LastEditedBy string `json:"last_edited_by"`
}
```
## 🔄 Development Workflows
### **Enhanced Development Workflow:**
```bash
# Start development iteration
insertr enhance ./mysite --site-id mysite
# Files are processed, content stored, elements marked as processed
# Subsequent enhancement runs skip already processed elements
# Developer can iterate on unprocessed parts without affecting existing content
# Start development server
insertr serve --dev-mode
# Editor changes -> only html_content updated, templates preserved
# Style detection uses stored original_template for consistency
```
### **Production Workflow:**
```bash
# Production enhancement (no DB cleanup)
insertr enhance ./mysite --site-id mysite
# Production server
insertr serve
# All editing preserves original developer templates
# StyleAware editor gets consistent style detection from stored templates
# Content updates only affect html_content field
```
## 🎯 Key Benefits
### **For Developers:**
**Efficient Processing**: Only process unprocessed elements, skip already handled ones
**Development Convenience**: Optional DB cleanup for fresh iterations
**HTML-first approach**: Direct alignment with StyleAware editor capabilities
**Zero Configuration**: Automatic detection and processing of viable elements
### **For Content Editors:**
**Style Preservation**: Developer styles always available via original_template
**Rich Editing**: Full HTML capabilities with formatting toolbar
**Perfect Fidelity**: No lossy conversions, complete attribute preservation
**Design Safety**: Cannot accidentally break developer styling constraints
### **For System Architecture:**
**Simplified Flow**: No markdown conversion complexity
**Direct Injection**: HTML content injects directly into static files
**Clean Separation**: Enhancement stores content, API serves editing
**Performance**: Skip already-processed elements for faster builds
## 📋 Implementation Tasks
### **Week 1: Database Foundation**
1. **Schema Updates**
- [ ] Update SQLite schema: replace `value` with `html_content`, add `original_template`
- [ ] Update PostgreSQL schema: replace `value` with `html_content`, add `original_template`
- [ ] Update `content.sql` queries to use new fields
- [ ] Regenerate SQLC models
2. **API Models**
- [ ] Update `ContentItem` struct to use `html_content` and `original_template`
- [ ] Update request/response structs for new field names
- [ ] Update API handlers to work with new field structure
### **Week 2: Enhancer Logic**
3. **First-Pass Processing**
- [x] Update enhancer to detect processed elements via `data-content-id`
- [x] Update enhancer to store `html_content` and `original_template` on first pass
- [x] Implement deterministic ID generation for consistent content injection
### **Week 3: Injector Redesign**
4. **HTML-Only Injection**
- [ ] Remove `MarkdownProcessor` and all markdown-related code from injector
- [ ] Update injector to use `html_content` directly via `injectHTMLContent()`
- [ ] Remove type-specific content processing (text, markdown, link)
### **Week 4: Integration Testing**
5. **StyleAware Editor Compatibility**
- [ ] Test API responses work correctly with StyleAware editor
- [ ] Verify `original_template` enables proper style detection
- [ ] Test rich HTML editing and injection end-to-end
## 🚀 Implementation Strategy
### **Priority Order:**
1. **Database Changes First**: Schema, queries, models - foundation for everything else
2. **Enhancer Updates**: First-pass processing logic and content storage
3. **Injector Simplification**: Remove markdown, use HTML directly
4. **Integration Testing**: Verify StyleAware editor compatibility
### **Key Implementation Notes:**
- **No Migration Required**: Fresh schema replacement, no backward compatibility needed
- **Enhancer-Driven**: Content storage happens during enhancement, not via API
- **HTML-Only**: Eliminate all markdown processing complexity
- **StyleAware Alignment**: API response format matches editor expectations exactly
This represents a fundamental shift to **HTML-first content management** with enhanced developer workflow efficiency while maintaining the zero-configuration philosophy that makes Insertr unique.
---
## ✅ IMPLEMENTATION COMPLETE
**Status**: ✅ **COMPLETED** - HTML-First Architecture Fully Functional
**Completion Date**: September 20, 2025
**Total Implementation Time**: Completed in 1 session
### **✅ What Was Successfully Implemented**
#### **Database Foundation (Week 1) - ✅ COMPLETE**
- ✅ Updated SQLite schema: replaced `value``html_content`, added `original_template`
- ✅ Updated PostgreSQL schema: replaced `value``html_content`, added `original_template`
- ✅ Updated `content.sql` queries to use new fields
- ✅ Regenerated SQLC models for both SQLite and PostgreSQL
#### **API Models - ✅ COMPLETE**
- ✅ Updated `ContentItem` struct to use `html_content` and `original_template`
- ✅ Updated request/response structs for new field structure
- ✅ Updated API handlers to work with new field structure
-**Added missing PUT handler** for content updates
#### **Enhancer Logic (Week 2) - ✅ COMPLETE**
- ✅ Updated enhancer to detect processed elements via `data-content-id`
- ✅ Implemented first-pass content storage with `html_content` and `original_template`
-**Implemented Enhanced Deterministic ID Generation**: 6-component signature system
-**Fixed ID Consistency**: Same elements get identical IDs across enhancement runs
- ✅ Successfully tested: 11 elements (simple), 39 elements (devigo) processed without errors
#### **Injector Redesign (Week 3) - ✅ COMPLETE**
-**Removed `MarkdownProcessor`** and all markdown processing code
- ✅ Updated injector to use `html_content` directly via `injectHTMLContent()`
-**Fixed critical HTML extraction/injection bugs**:
- `extractHTMLContent()`: Now renders all child nodes correctly
- `injectContentIntoNode()`: Now properly parses HTML instead of escaping
-**Refactored architecture**: Engine now uses injector instead of duplicating logic
#### **Integration & Bug Fixes (Week 4) - ✅ COMPLETE**
-**Fixed API-Frontend Integration**: Updated `api-client.js` to use `html_content` field
-**Added missing `updateContent()` method** to frontend API client
-**Created `UpdateContent` HTTP handler** for PUT requests
-**Fixed content type validation**: Updated `detectContentType()` to only return database-valid types (`text`/`link`)
-**Added PUT route to server configuration**
-**Verified complete end-to-end workflow**: Enhancement → API → Editor → Persistence
### **🚀 Current System Capabilities**
The HTML-First Architecture now provides:
1. **✅ Complete CRUD API**: GET, POST, PUT operations with version control
2. **✅ Rich HTML Storage**: Direct HTML content storage with perfect attribute preservation
3. **✅ Template Preservation**: Original developer templates stored for style detection
4. **✅ First-Pass Processing**: Only unprocessed elements are enhanced, skip existing ones
5. **✅ StyleAware Editor Compatibility**: API responses match editor expectations exactly
6. **✅ Content Type validation**: Only valid database types (`text`/`link`) accepted
7. **✅ Server Recovery**: Complete HTTP server with all routes functional
8. **✅ Enhanced Deterministic ID Generation**: Consistent IDs enable reliable content injection
9. **✅ Enhance Button Workflow**: Complete API edit → enhance trigger → static file injection
### **🎯 Verified End-to-End Workflow**
```bash
# ✅ Content Creation (POST)
curl -X POST /api/content → Creates content with proper ID generation
# ✅ Content Updates (PUT)
curl -X PUT /api/content/{id} → Updates content and timestamps correctly
# ✅ Content Retrieval (GET)
curl -X GET /api/content/{id} → Returns current content with proper field names
```
### **📊 Implementation Results**
**Files Modified**: 37 files
**Lines Changed**: +1,197, -745 (net +452 lines)
**Key Deletions**: `internal/engine/markdown.go` (76 lines) - Complete markdown removal
**Key Additions**:
- `UpdateContent` HTTP handler (85+ lines)
- Enhanced Deterministic ID Generation system (63+ lines)
- Complete HTML-first database schema
- Frontend payload parsing and update/create logic
**Breaking Changes**: Yes (fresh schema, but complete backward compatibility through migration path)
---
## 🎯 **PHASE 3A FINAL STATUS**
**Status**: ✅ **COMPLETE & PRODUCTION READY**
**Final Achievement**: Complete HTML-first content management system with enhance button functionality
### **🎉 Key Milestones Achieved**
- **HTML-First Architecture**: Complete transition from text-based to HTML-native storage
- **Deterministic ID System**: Reliable content injection with consistent element identification
- **Enhance Button Workflow**: End-to-end content editing and static file injection
- **StyleAware Editor Integration**: Perfect compatibility with frontend rich editing capabilities
- **Production Readiness**: All critical workflows verified and operational
**Phase 3a represents a successful transformation to modern HTML-first content management.**

View File

@@ -1,90 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Debug Style Detection</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 2rem;
line-height: 1.6;
}
.fancy {
color: #7c3aed;
text-decoration: none;
border-bottom: 2px solid #a855f7;
}
.fancy:hover {
background: #ede9fe;
}
.test-output {
background: #f5f5f5;
padding: 1rem;
border-radius: 8px;
font-family: 'Monaco', 'Courier New', monospace;
white-space: pre-wrap;
max-height: 500px;
overflow-y: auto;
margin-top: 1rem;
}
</style>
</head>
<body>
<h1>🔍 Debug Style Detection</h1>
<p>Testing what styles are detected for Example 2:</p>
<!-- This is Example 2 from the simple demo -->
<p id="example2" class="insertr">Visit our <a class="fancy" href="#about">about page</a> for more info.</p>
<button onclick="debugStyleDetection()">Debug Style Detection</button>
<div id="output" class="test-output">Click the button to see debug output...</div>
<script type="module">
import { styleDetectionEngine } from './lib/src/utils/style-detection.js';
window.debugStyleDetection = function() {
const element = document.getElementById('example2');
const output = document.getElementById('output');
console.log('🔍 Debugging style detection for:', element.innerHTML);
// Run style detection
const result = styleDetectionEngine.detectStylesAndStructure(element);
let debugInfo = '🔍 Style Detection Debug Results\n';
debugInfo += '================================\n\n';
debugInfo += `Element HTML: ${element.innerHTML}\n\n`;
debugInfo += `Detected Styles (${result.styles.size}):\n`;
debugInfo += '-------------------------\n';
for (const [styleId, styleInfo] of result.styles) {
debugInfo += `Style ID: ${styleId}\n`;
debugInfo += ` Name: ${styleInfo.name}\n`;
debugInfo += ` Tag: ${styleInfo.tagName}\n`;
debugInfo += ` Classes: [${styleInfo.classes.join(', ')}]\n`;
debugInfo += ` Attributes: ${JSON.stringify(styleInfo.attributes)}\n\n`;
}
debugInfo += `Content Structure (${result.structure.length} pieces):\n`;
debugInfo += '----------------------------\n';
result.structure.forEach((piece, index) => {
debugInfo += `${index}: ${piece.type}`;
if (piece.type === 'styled') {
debugInfo += ` (styleId: ${piece.styleId})`;
}
if (piece.content) {
debugInfo += ` - "${piece.content}"`;
}
debugInfo += '\n';
});
output.textContent = debugInfo;
console.log(debugInfo);
};
</script>
</body>
</html>

View File

@@ -1,19 +1,20 @@
<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>About - Acme Consulting Services</title>
<link rel="stylesheet" href="assets/style.css"/>
<script src="http://localhost:8080/insertr.js" data-insertr-injected="true" data-site-id="demo" data-api-endpoint="http://localhost:8080/api/content" data-mock-auth="true" data-debug="true"></script></head>
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<!-- Navigation -->
<nav class="navbar">
<div class="container">
<h1 class="logo insertr" data-content-id="about-logo-bf9558" data-content-type="text">Acme Consulting</h1>
<h1 class="logo insertr">Acme Consulting</h1>
<ul class="nav-links">
<li class="insertr" data-content-id="about-li-0babbf" data-content-type="text"><a href="index.html">Home</a></li>
<li class="insertr" data-content-id="about-li-2-0babbf" data-content-type="text"><a href="about.html">About</a></li>
<li class="insertr" data-content-id="about-li-3-0babbf" data-content-type="text"><a href="contact.html">Contact</a></li>
<li class="insertr"><a href="index.html">Home</a></li>
<li class="insertr"><a href="about.html">About</a></li>
<li class="insertr"><a href="contact.html">Contact</a></li>
</ul>
</div>
@@ -22,21 +23,21 @@
<!-- Hero Section -->
<section class="hero">
<div class="container">
<h1 class="insertr" data-content-id="about-h1-b0851a" data-content-type="text">About Acme Consulting</h1>
<p class="lead insertr" data-content-id="about-lead-ccc316" data-content-type="markdown">We&#39;re a team of experienced consultants dedicated to helping small businesses thrive in today&#39;s competitive marketplace.</p>
<h1 class="insertr">About Acme Consulting</h1>
<p class="lead insertr">We're a team of experienced consultants dedicated to helping small businesses thrive in today's competitive marketplace.</p>
</div>
</section>
<!-- Story Section -->
<section class="services">
<div class="container">
<h2 class="insertr" data-content-id="about-h2-246854" data-content-type="text">Our Story</h2>
<h2 class="insertr">Our Story</h2>
<div class="insertr-group">
<p class="insertr" data-content-id="about-p-b2f44a" data-content-type="markdown">Founded in 2020, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.</p>
<p class="insertr">Founded in 2020, Acme Consulting emerged from a simple observation: small businesses needed access to the same high-quality strategic advice that large corporations receive, but in a format that was accessible, affordable, and actionable.</p>
<p class="insertr" data-content-id="about-p-2-b2f44a" data-content-type="markdown">Our founders, with combined experience of over 30 years in business strategy, operations, and technology, recognized that the traditional consulting model wasn&#39;t serving the needs of growing businesses. We set out to change that.</p>
<p class="insertr">Our founders, with combined experience of over 30 years in business strategy, operations, and technology, recognized that the traditional consulting model wasn't serving the needs of growing businesses. We set out to change that.</p>
<p class="insertr" data-content-id="about-p-3-b2f44a" data-content-type="markdown">Today, we&#39;ve helped over **200 businesses** streamline their operations, clarify their strategy, and achieve sustainable growth. Our approach combines proven methodologies with a deep understanding of the unique challenges facing small to medium-sized businesses.</p>
<p class="insertr">Today, we've helped over **200 businesses** streamline their operations, clarify their strategy, and achieve sustainable growth. Our approach combines proven methodologies with a deep understanding of the unique challenges facing small to medium-sized businesses.</p>
</div>
</div>
</section>
@@ -44,26 +45,26 @@
<!-- Team Section -->
<section class="cta">
<div class="container">
<h2 class="insertr" data-content-id="about-h2-2-f16ab1" data-content-type="text">Our Team</h2>
<p class="insertr" data-content-id="about-p-0e26bc" data-content-type="markdown">We&#39;re a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.</p>
<h2 class="insertr">Our Team</h2>
<p class="insertr">We're a diverse group of strategists, operators, and technology experts united by our passion for helping businesses succeed.</p>
<div class="services-grid" style="margin-top: 3rem;">
<div class="service-card">
<div class="insertr" data-content-id="about-div-dac2cd" data-content-type="markdown">
<div class="insertr">
<h3>Sarah Chen</h3>
<p><strong>Founder &amp; CEO</strong></p>
<p><strong>Founder & CEO</strong></p>
<p>Former <strong>McKinsey consultant</strong> with 15 years of experience in strategy and operations. MBA from Stanford.</p>
</div>
</div>
<div class="service-card">
<div class="insertr" data-content-id="about-div-2-dac2cd" data-content-type="markdown">
<div class="insertr">
<h3>Michael Rodriguez</h3>
<p><strong>Head of Operations</strong></p>
<p>20 years in manufacturing and supply chain optimization. Expert in <strong>lean methodologies</strong> and process improvement.</p>
</div>
</div>
<div class="service-card">
<div class="insertr" data-content-id="about-div-3-dac2cd" data-content-type="markdown">
<div class="insertr">
<h3>Emma Thompson</h3>
<p><strong>Digital Strategy Lead</strong></p>
<p>Former tech startup founder turned consultant. Specializes in <em>digital transformation</em> and technology adoption.</p>
@@ -76,19 +77,19 @@
<!-- Values Section -->
<section class="testimonial">
<div class="container">
<h2 class="insertr" style="margin-bottom: 2rem;" data-content-id="about-h2-3-893efa" data-content-type="text">Our Values</h2>
<h2 class="insertr" style="margin-bottom: 2rem;">Our Values</h2>
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem; text-align: left;">
<div>
<h3 class="insertr" data-content-id="about-h3-4-07ce1b" data-content-type="text">Client-First</h3>
<p class="insertr" data-content-id="about-p-2-9f60dd" data-content-type="markdown">Every recommendation we make is designed with your specific business context and goals in mind.</p>
<h3 class="insertr">Client-First</h3>
<p class="insertr">Every recommendation we make is designed with your specific business context and goals in mind.</p>
</div>
<div>
<h3 class="insertr" data-content-id="about-h3-5-07ce1b" data-content-type="text">Practical Solutions</h3>
<p class="insertr" data-content-id="about-p-3-9f60dd" data-content-type="markdown">We believe in strategies that you can actually implement with your current resources and capabilities.</p>
<h3 class="insertr">Practical Solutions</h3>
<p class="insertr">We believe in strategies that you can actually implement with your current resources and capabilities.</p>
</div>
<div>
<h3 class="insertr" data-content-id="about-h3-6-07ce1b" data-content-type="text">Long-term Partnership</h3>
<p class="insertr" data-content-id="about-p-4-9f60dd" data-content-type="markdown">We&#39;re not just consultants; we&#39;re partners in your business success for the long haul.</p>
<h3 class="insertr">Long-term Partnership</h3>
<p class="insertr">We're not just consultants; we're partners in your business success for the long haul.</p>
</div>
</div>
</div>
@@ -97,12 +98,12 @@
<!-- Test Section for Insertr Features -->
<section class="testimonial">
<div class="container">
<h2 class="insertr" data-content-id="about-h2-4-893efa" data-content-type="text">Feature Tests</h2>
<h2 class="insertr">Feature Tests</h2>
<!-- Test 1: .insertr container expansion (should make each p individually editable) -->
<div style="margin-bottom: 2rem;">
<h3 class="insertr" data-content-id="about-h3-ea6b29" data-content-type="text">Test 1: Container Expansion (.insertr)</h3>
<div class="insertr" style="border: 2px dashed #ccc; padding: 1rem;" data-content-id="about-div-4-e2aa93" data-content-type="markdown">
<h3 class="insertr">Test 1: Container Expansion (.insertr)</h3>
<div class="insertr" style="border: 2px dashed #ccc; padding: 1rem;">
<p>This paragraph should be individually editable with a textarea.</p>
<p>This second paragraph should also be individually editable.</p>
<p>Each paragraph should get its own modal when clicked.</p>
@@ -111,26 +112,31 @@
<!-- Test 2: .insertr-group collective editing (should edit all together) -->
<div>
<h3 class="insertr" data-content-id="about-h3-2-ea6b29" data-content-type="text">Test 2: Group Editing (.insertr-group)</h3>
<h3 class="insertr">Test 2: Group Editing (.insertr-group)</h3>
<div class="insertr-group" style="border: 2px solid #007cba; padding: 1rem;">
<p class="insertr" data-content-id="about-p-4-dcfaf1" data-content-type="markdown">This paragraph is part of a <strong>group</strong>.</p>
<p class="insertr" data-content-id="about-p-5-dcfaf1" data-content-type="markdown">Clicking anywhere should open one markdown editor with <em>rich formatting</em>.</p>
<p class="insertr" data-content-id="about-p-6-dcfaf1" data-content-type="markdown">All content should be <strong>editable together</strong> as markdown with proper <em>HTML conversion</em>.</p>
<p class="insertr">This paragraph is part of a <strong>group</strong>.</p>
<p class="insertr">Clicking anywhere should open one markdown editor with <em>rich formatting</em>.</p>
<p class="insertr">All content should be <strong>editable together</strong> as markdown with proper <em>HTML conversion</em>.</p>
</div>
</div>
<!-- Test 3: Link formatting options -->
<div style="margin-top: 2rem;">
<h3 class="insertr">Test 3: Link Formatting Styles</h3>
<p class="insertr">Visit our <a href="index.html" class="link-primary">home page</a> to learn more about our services. You can also check our <a href="#services" class="link-secondary">services section</a> for detailed information. For special offers, see our <a href="#cta" class="link-accent">consultation page</a>.</p>
</div>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="container">
<p class="insertr" data-content-id="about-p-8-c093f3" data-content-type="markdown">© 2024 Acme Consulting Services. All rights reserved.</p>
<p class="insertr" data-content-id="about-p-9-c093f3" data-content-type="markdown">📧 info@acmeconsulting.com | 📞 (555) 123-4567 | <button class="insertr-gate" style="background: none; border: 1px solid #ccc; padding: 4px 8px; margin-left: 10px; border-radius: 3px; font-size: 11px;">🔧 Edit</button></p>
<p class="insertr">© 2024 Acme Consulting Services. All rights reserved.</p>
<p class="insertr">📧 info@acmeconsulting.com | 📞 (555) 123-4567</p>
</div>
</footer>
<!-- Insertr JavaScript Library -->
<script type="text/javascript" src="insertr.js"></script>

View File

@@ -211,6 +211,44 @@ body {
margin-right: auto;
}
/* Default Link Formatting Styles */
.link-primary {
color: #2563eb;
text-decoration: underline;
font-weight: 500;
transition: color 0.3s;
}
.link-primary:hover {
color: #1d4ed8;
}
.link-secondary {
color: #6b7280;
text-decoration: none;
border-bottom: 1px solid #d1d5db;
transition: border-color 0.3s;
}
.link-secondary:hover {
border-bottom-color: #374151;
}
.link-accent {
color: #7c3aed;
text-decoration: none;
background: linear-gradient(120deg, #7c3aed 0%, #7c3aed 100%);
background-repeat: no-repeat;
background-size: 100% 0.2em;
background-position: 0 88%;
transition: background-size 0.25s ease-in;
}
.link-accent:hover {
background-size: 100% 88%;
color: white;
}
/* Footer */
.footer {
background: #1f2937;

View File

@@ -1,18 +1,20 @@
<!DOCTYPE html><html lang="en"><head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Acme Consulting Services - Live Reload Test</title>
<link rel="stylesheet" href="assets/style.css"/>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Acme Consulting Services</title>
<link rel="stylesheet" href="assets/style.css">
</head>
<body>
<!-- Navigation -->
<nav class="navbar">
<div class="container">
<h1 class="logo insertr" data-content-id="index-logo-c176ba" data-content-type="text">Acme Consulting</h1>
<h1 class="logo">Acme Consulting</h1>
<ul class="nav-links">
<li class="insertr" data-content-id="index-li-bf7136" data-content-type="text"><a href="index.html">Home</a></li>
<li class="insertr" data-content-id="index-li-2-bf7136" data-content-type="text"><a href="about.html">About</a></li>
<li class="insertr" data-content-id="index-li-3-bf7136" data-content-type="text"><a href="contact.html">Contact</a></li>
<li class=""><a href="index.html">Home</a></li>
<li class=""><a href="about.html">About</a></li>
<li class=""><a href="contact.html">Contact</a></li>
</ul>
</div>
@@ -21,30 +23,30 @@
<!-- Hero Section -->
<section class="hero">
<div class="container">
<h1 class="insertr" data-content-id="index-h1-1b83cf" data-content-type="text">Transform Your Business with Expert Consulting</h1>
<p class="lead insertr" data-content-id="index-lead-c7070a" data-content-type="markdown"><strong>We help small</strong> businesses grow through strategic planning, process optimization, and digital transformation. Our team brings 15+ years of experience to drive your success.</p>
<a href="contact.html" class="btn-primary insertr" data-content-id="index-btn-primary-088a84" data-content-type="link">Get Started Today</a>
<h1 class="insertr">Transform Your Business with Expert Consulting</h1>
<p class="lead insertr"><strong>We help small</strong> businesses grow through strategic planning, process optimization, and digital transformation. Our team brings 15+ years of experience to drive your success.</p>
<a href="contact.html" class="btn-primary insertr">Get Started Today</a>
</div>
</section>
<!-- Services Section -->
<section class="services">
<div class="container">
<h2 class="insertr" data-content-id="index-h2-7e9e73" data-content-type="text">Our Services</h2>
<p class="section-subtitle insertr" data-content-id="index-section-subtitle-bf0683" data-content-type="markdown">Comprehensive solutions tailored to your business needs. We are flexible as willow sticks</p>
<h2 class="insertr">Our <em>Services</em></h2>
<p class="section-subtitle insertr">Comprehensive solutions tailored to your business needs</p>
<div class="services-grid">
<div class="service-card">
<h3 class="insertr" data-content-id="index-h3-2c6736" data-content-type="text">Strategic Planning</h3>
<p class="insertr" data-content-id="index-p-a935d2" data-content-type="markdown">Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.</p>
<h3 class="insertr">Strategic Planning</h3>
<p class="insertr">Develop clear roadmaps and actionable strategies that align with your business goals and drive sustainable growth.</p>
</div>
<div class="service-card">
<h3 class="insertr" data-content-id="index-h3-2-2c6736" data-content-type="text">Operations Optimization</h3>
<p class="insertr" data-content-id="index-p-2-a935d2" data-content-type="markdown">Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.</p>
<h3 class="insertr">Operations Optimization</h3>
<p class="insertr">Streamline processes, reduce costs, and improve efficiency through proven methodologies and best practices.</p>
</div>
<div class="service-card">
<h3 class="insertr" data-content-id="index-h3-3-2c6736" data-content-type="text">Digital Transformation</h3>
<p class="insertr" data-content-id="index-p-3-a935d2" data-content-type="markdown">Modernize your technology stack and digital presence to compete effectively in today&#39;s marketplace.</p>
<h3 class="insertr">Digital Transformation</h3>
<p class="insertr">Modernize your technology stack and digital presence to compete effectively in today's marketplace.</p>
</div>
</div>
</div>
@@ -54,8 +56,8 @@
<section class="testimonial">
<div class="container">
<blockquote>
<p class="insertr" data-content-id="index-p-4-0a9466" data-content-type="markdown">&#34;Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations.&#34;</p>
<cite class="insertr" data-content-id="index-cite-24152c" data-content-type="text">Sarah Johnson, CEO of TechStart Inc.</cite>
<p class="insertr">"Acme Consulting transformed our operations completely. We saw a 40% increase in efficiency within 6 months of implementing their recommendations."</p>
<cite class="insertr">Sarah Johnson, CEO of TechStart Inc.</cite>
</blockquote>
</div>
</section>
@@ -63,17 +65,17 @@
<!-- Call to Action -->
<section class="cta">
<div class="container">
<h2 class="insertr" data-content-id="index-h2-2-9b9baa" data-content-type="text">Ready to Transform Your Business?</h2>
<p class="insertr" data-content-id="index-p-5-e960fe" data-content-type="markdown">Contact us today for a free consultation and discover how we can help you achieve your goals.</p>
<a href="contact.html" class="btn-primary insertr" data-content-id="index-btn-primary-2-a33c64" data-content-type="link">Schedule Consultation</a>
<h2 class="insertr">Ready to Transform Your Business?</h2>
<p class="insertr">Contact us today for a free consultation and discover how we can help you achieve your goals.</p>
<a href="contact.html" class="btn-primary insertr">Schedule Consultation</a>
</div>
</section>
<!-- Footer -->
<footer class="footer">
<div class="container">
<p class="insertr" data-content-id="index-p-6-9b47e7" data-content-type="markdown">© 2024 Acme Consulting Services. All rights reserved.</p>
<p class="insertr" data-content-id="index-p-7-9b47e7" data-content-type="markdown">📧 info@acmeconsulting.com | 📞 (555) 123-4567 | Located in downtown Springfield | <button class="insertr-gate" style="background: none; border: 1px solid #ccc; padding: 4px 8px; margin-left: 10px; border-radius: 3px; font-size: 11px;">🔧 Edit</button></p>
<p class="insertr">© 2024 Acme Consulting Services. All rights reserved.</p>
<p class="insertr">📧 info@acmeconsulting.com | 📞 (555) 123-4567 | Located in downtown Springfield</p>
</div>
</footer>

View File

@@ -146,18 +146,9 @@
<p class="insertr">Ready to start? <button class="btn" data-action="signup" data-analytics="cta-main">Sign Up Now</button> and begin your journey!</p>
</div>
<!-- Example 6: Nested Styled Elements -->
<!-- Example 6: Card Structure (Complex) -->
<div class="example">
<h3>Example 6: Nested Styled Elements</h3>
<div class="example-description">
Tests complex nesting with multiple levels of styling.
</div>
<p class="insertr">Check out our <a href="#products" class="fancy">latest <strong class="emph">products</strong> and <span class="highlight">deals</span></a> today!</p>
</div>
<!-- Example 7: Card Structure (Complex) -->
<div class="example">
<h3>Example 7: Complex Structure</h3>
<h3>Example 6: Complex Structure</h3>
<div class="example-description">
Tests editing of a more complex structure with multiple elements. This might be too complex for markdown.
</div>
@@ -167,18 +158,9 @@
</div>
</div>
<!-- Example 8: Simple Text for Comparison -->
<!-- Example 7: Link with Class and ID -->
<div class="example">
<h3>Example 8: Simple Text (Control)</h3>
<div class="example-description">
Simple text without any styling - should work perfectly with markdown.
</div>
<p class="insertr">This is just plain text with some basic markdown formatting like bold and italic.</p>
</div>
<!-- Example 9: Link with Class and ID -->
<div class="example">
<h3>Example 9: Link with Multiple Attributes</h3>
<h3>Example 7: Link with Multiple Attributes</h3>
<div class="example-description">
Tests preservation of multiple attributes including id, class, and title.
</div>

View File

@@ -1,26 +0,0 @@
# Test configuration for Authentik integration
dev_mode: true
database:
path: "./insertr-test.db"
server:
port: 8080
sites:
- site_id: "default"
path: "./demos/default_enhanced"
source_path: "./demos/default"
auto_enhance: true
cli:
site_id: "default"
output: "./dist"
inject_demo_gate: true
auth:
provider: "mock" # Change this to test different providers
jwt_secret: "test-secret-change-in-production"
oidc:
endpoint: "https://auth.example.com/application/o/insertr/"
client_id: "insertr-test-client"
client_secret: "test-secret"

View File

@@ -36,7 +36,6 @@
/* Background colors */
--insertr-bg-primary: #ffffff;
--insertr-bg-secondary: #f8f9fa;
--insertr-bg-dark: #343a40;
--insertr-bg-overlay: rgba(0, 0, 0, 0.5);
/* Border and spacing */
@@ -48,12 +47,8 @@
--insertr-spacing-lg: 24px;
/* Z-index management */
--insertr-z-dropdown: 1000;
--insertr-z-sticky: 1020;
--insertr-z-fixed: 1030;
--insertr-z-modal-backdrop: 1040;
--insertr-z-modal: 1050;
--insertr-z-popover: 1060;
--insertr-z-tooltip: 1070;
--insertr-z-overlay: 999999;
@@ -61,14 +56,9 @@
--insertr-font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
--insertr-font-size-sm: 12px;
--insertr-font-size-base: 14px;
--insertr-font-size-lg: 16px;
--insertr-line-height: 1.4;
/* Form elements */
--insertr-input-padding: 0.75rem;
--insertr-input-border: 1px solid var(--insertr-border-color);
--insertr-input-border-focus: 1px solid var(--insertr-primary);
--insertr-input-bg: #ffffff;
/* Form elements - using existing variables */
/* Animation */
--insertr-transition: all 0.2s ease-in-out;
@@ -380,16 +370,16 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
transform: translateY(1px);
}
/* Style preview buttons - styled dynamically via JavaScript */
/* Style preview buttons - new approach using CSS isolation */
.insertr-style-btn.insertr-style-preview {
/* Preserve button structure */
background: var(--insertr-bg-primary) !important;
border: 1px solid var(--insertr-border-color) !important;
border-radius: var(--insertr-border-radius) !important;
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm) !important;
font-size: var(--insertr-font-size-sm) !important;
cursor: pointer !important;
transition: var(--insertr-transition) !important;
/* Clean button container - minimal styling */
background: var(--insertr-bg-primary);
border: 1px solid var(--insertr-border-color);
border-radius: var(--insertr-border-radius);
padding: 0; /* Remove padding - let preview content handle it */
font-size: var(--insertr-font-size-sm);
cursor: pointer;
transition: var(--insertr-transition);
/* Button layout */
min-height: 28px;
@@ -397,37 +387,59 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
align-items: center;
justify-content: center;
/* Ensure button remains clickable */
position: relative;
/* Reset any inherited text styles on the button container only */
font-family: var(--insertr-font-family);
text-decoration: none;
font-weight: normal;
text-transform: none;
/* Styles will be applied dynamically via JavaScript */
/* Don't set color here - let the preview content inherit naturally */
/* Ensure content fits */
overflow: hidden;
}
/* Add subtle background to preview buttons to ensure they remain clickable-looking */
.insertr-style-btn.insertr-style-preview::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--insertr-bg-primary);
opacity: 0.9;
border-radius: inherit;
z-index: -1;
/* Preview content container - minimal interference with original styling */
.insertr-preview-content {
/* Allow the original classes to style this element completely naturally */
display: inline-block;
/* Only set essential layout properties */
box-sizing: border-box;
/* Ensure text fits within button */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 120px;
/* Inherit font size from button container */
font-size: inherit;
/* Remove browser defaults that might interfere - but don't override intentional styling */
border: none;
margin: 0;
/* NO background, color, padding defaults - let the classes handle everything */
}
/* Hover state for preview buttons */
/* Minimal fallback styling when no meaningful classes are detected */
.insertr-preview-content.insertr-fallback-style {
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
color: var(--insertr-text-primary);
}
/* Hover state for preview buttons - subtle visual feedback */
.insertr-style-btn.insertr-style-preview:hover {
background: var(--insertr-bg-secondary) !important;
border-color: var(--insertr-text-muted) !important;
transform: none !important;
border-color: var(--insertr-text-muted);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Active state for preview buttons */
.insertr-style-btn.insertr-style-preview:active {
background: var(--insertr-border-color) !important;
transform: translateY(1px) !important;
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* Editor components */
@@ -435,14 +447,14 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
.insertr-rich-editor,
.insertr-fallback-textarea {
width: 100%;
border: var(--insertr-input-border);
border: 1px solid var(--insertr-border-color);
border-radius: var(--insertr-border-radius);
padding: var(--insertr-input-padding);
padding: var(--insertr-spacing-md);
font-size: var(--insertr-font-size-base);
line-height: var(--insertr-line-height);
font-family: var(--insertr-font-family);
color: var(--insertr-text-primary);
background: var(--insertr-input-bg);
background: var(--insertr-bg-primary);
margin-bottom: var(--insertr-spacing-md);
transition: var(--insertr-transition);
box-sizing: border-box;
@@ -452,7 +464,7 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
.insertr-rich-editor:focus,
.insertr-fallback-textarea:focus {
outline: none;
border: var(--insertr-input-border-focus);
border: 1px solid var(--insertr-primary);
box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
}

View File

@@ -72,14 +72,13 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
processedElements[i] = ProcessedElement{
Node: elem.Node,
ID: id,
Type: elem.Type,
Generated: !contentExists, // Mark as generated only if new to database
Tag: elem.Node.Data,
Classes: GetClasses(elem.Node),
}
// Add/update content attributes to the node
e.addContentAttributes(elem.Node, id, elem.Type)
// Add/update content attributes to the node (only content-id now)
e.addContentAttributes(elem.Node, id)
// Store content only for truly new elements (database-first check)
if !contentExists && (input.Mode == Enhancement || input.Mode == ContentInjection) {
@@ -88,12 +87,12 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
originalTemplate := e.extractOriginalTemplate(elem.Node)
// Store in database via content client
_, err := e.client.CreateContent(input.SiteID, id, htmlContent, originalTemplate, elem.Type, "system")
_, err := e.client.CreateContent(input.SiteID, id, htmlContent, originalTemplate, "system")
if err != nil {
// Log error but don't fail the enhancement - content just won't be stored
fmt.Printf("⚠️ Failed to store content for %s: %v\n", id, err)
} else {
fmt.Printf("✅ Created new content: %s (%s)\n", id, elem.Type)
fmt.Printf("✅ Created new content: %s (html)\n", id)
}
}
}
@@ -122,21 +121,47 @@ func (e *ContentEngine) ProcessContent(input ContentInput) (*ContentResult, erro
// InsertrElement represents an insertr element found in HTML
type InsertrElement struct {
Node *html.Node
Type string
}
// findInsertrElements finds all elements with class="insertr"
// findInsertrElements finds all elements with class="insertr" and applies container transformation
// This implements the "syntactic sugar transformation" from CLASSES.md:
// - Containers with .insertr get their .insertr class removed
// - Viable children of those containers get .insertr class added
// - Regular elements with .insertr are kept as-is
func (e *ContentEngine) findInsertrElements(doc *html.Node) []InsertrElement {
var elements []InsertrElement
var containersToTransform []*html.Node
// First pass: find all .insertr elements and identify containers
e.walkNodes(doc, func(n *html.Node) {
if n.Type == html.ElementNode && e.hasInsertrClass(n) {
elementType := e.determineContentType(n)
elements = append(elements, InsertrElement{
Node: n,
Type: elementType,
})
if isContainer(n) {
// Container element - mark for transformation
containersToTransform = append(containersToTransform, n)
} else {
// Regular element - add directly
elements = append(elements, InsertrElement{
Node: n,
})
}
}
})
// Second pass: transform containers (remove .insertr from container, add to children)
for _, container := range containersToTransform {
// Remove .insertr class from container
e.removeClass(container, "insertr")
// Find viable children and add .insertr class to them
viableChildren := FindViableChildren(container)
for _, child := range viableChildren {
e.addClass(child, "insertr")
elements = append(elements, InsertrElement{
Node: child,
})
}
}
return elements
}
@@ -159,28 +184,11 @@ func (e *ContentEngine) hasInsertrClass(node *html.Node) bool {
return false
}
// determineContentType determines the content type based on element
func (e *ContentEngine) determineContentType(node *html.Node) string {
tag := strings.ToLower(node.Data)
switch tag {
case "a", "button":
return "link"
case "h1", "h2", "h3", "h4", "h5", "h6":
return "text"
case "p", "div", "section", "article", "span":
return "text"
default:
return "text"
}
}
// addContentAttributes adds data-content-id and data-content-type attributes
func (e *ContentEngine) addContentAttributes(node *html.Node, contentID, contentType string) {
// addContentAttributes adds data-content-id attribute only
// HTML-first approach: no content-type attribute needed
func (e *ContentEngine) addContentAttributes(node *html.Node, contentID string) {
// Add data-content-id attribute
e.setAttribute(node, "data-content-id", contentID)
// Add data-content-type attribute
e.setAttribute(node, "data-content-type", contentType)
}
// getAttribute gets an attribute value from an HTML node
@@ -209,6 +217,85 @@ func (e *ContentEngine) setAttribute(node *html.Node, key, value string) {
})
}
// addClass safely adds a class to an HTML node
func (e *ContentEngine) addClass(node *html.Node, className string) {
var classAttr *html.Attribute
var classIndex int = -1
// Find existing class attribute
for idx, attr := range node.Attr {
if attr.Key == "class" {
classAttr = &attr
classIndex = idx
break
}
}
var classes []string
if classAttr != nil {
classes = strings.Fields(classAttr.Val)
}
// Check if class already exists
for _, class := range classes {
if class == className {
return // Class already exists
}
}
// Add new class
classes = append(classes, className)
newClassValue := strings.Join(classes, " ")
if classIndex >= 0 {
// Update existing class attribute
node.Attr[classIndex].Val = newClassValue
} else {
// Add new class attribute
node.Attr = append(node.Attr, html.Attribute{
Key: "class",
Val: newClassValue,
})
}
}
// removeClass safely removes a class from an HTML node
func (e *ContentEngine) removeClass(node *html.Node, className string) {
var classIndex int = -1
// Find existing class attribute
for idx, attr := range node.Attr {
if attr.Key == "class" {
classIndex = idx
break
}
}
if classIndex == -1 {
return // No class attribute found
}
// Parse existing classes
classes := strings.Fields(node.Attr[classIndex].Val)
// Filter out the target class
var newClasses []string
for _, class := range classes {
if class != className {
newClasses = append(newClasses, class)
}
}
// Update or remove class attribute
if len(newClasses) == 0 {
// Remove class attribute entirely if no classes remain
node.Attr = append(node.Attr[:classIndex], node.Attr[classIndex+1:]...)
} else {
// Update class attribute with remaining classes
node.Attr[classIndex].Val = strings.Join(newClasses, " ")
}
}
// injectContent injects content from database into elements
func (e *ContentEngine) injectContent(elements []ProcessedElement, siteID string) error {
for i := range elements {

View File

@@ -10,111 +10,23 @@ export class InsertrCore {
};
}
// Find all enhanced elements on the page with container expansion
// Find all enhanced elements on the page
// Note: Container expansion is handled at build-time by the backend enhancer
// Frontend should only find elements that already have .insertr class
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;
return document.querySelectorAll('.insertr');
}
// 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());
}
// Note: All container expansion logic removed - handled by backend enhancer
// Frontend only works with elements that already have .insertr class
// 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
// HTML-first approach: no content type needed, just HTML markup for 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
};
@@ -130,38 +42,10 @@ export class InsertrCore {
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
// Get all elements with their metadata
// Note: Container expansion handled by backend - frontend finds enhanced elements only
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));
const elements = this.findEnhancedElements();
return Array.from(elements).map(el => this.getElementMetadata(el));
}
}

View File

@@ -370,16 +370,16 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
transform: translateY(1px);
}
/* Style preview buttons - styled dynamically via JavaScript */
/* Style preview buttons - new approach using CSS isolation */
.insertr-style-btn.insertr-style-preview {
/* Preserve button structure */
background: var(--insertr-bg-primary) !important;
border: 1px solid var(--insertr-border-color) !important;
border-radius: var(--insertr-border-radius) !important;
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm) !important;
font-size: var(--insertr-font-size-sm) !important;
cursor: pointer !important;
transition: var(--insertr-transition) !important;
/* Clean button container - minimal styling */
background: var(--insertr-bg-primary);
border: 1px solid var(--insertr-border-color);
border-radius: var(--insertr-border-radius);
padding: 0; /* Remove padding - let preview content handle it */
font-size: var(--insertr-font-size-sm);
cursor: pointer;
transition: var(--insertr-transition);
/* Button layout */
min-height: 28px;
@@ -387,37 +387,59 @@ body:not(.insertr-edit-mode) .insertr-editing-hover::after {
align-items: center;
justify-content: center;
/* Ensure button remains clickable */
position: relative;
/* Reset any inherited text styles on the button container only */
font-family: var(--insertr-font-family);
text-decoration: none;
font-weight: normal;
text-transform: none;
/* Styles will be applied dynamically via JavaScript */
/* Don't set color here - let the preview content inherit naturally */
/* Ensure content fits */
overflow: hidden;
}
/* Add subtle background to preview buttons to ensure they remain clickable-looking */
.insertr-style-btn.insertr-style-preview::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: var(--insertr-bg-primary);
opacity: 0.9;
border-radius: inherit;
z-index: -1;
/* Preview content container - minimal interference with original styling */
.insertr-preview-content {
/* Allow the original classes to style this element completely naturally */
display: inline-block;
/* Only set essential layout properties */
box-sizing: border-box;
/* Ensure text fits within button */
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
max-width: 120px;
/* Inherit font size from button container */
font-size: inherit;
/* Remove browser defaults that might interfere - but don't override intentional styling */
border: none;
margin: 0;
/* NO background, color, padding defaults - let the classes handle everything */
}
/* Hover state for preview buttons */
/* Minimal fallback styling when no meaningful classes are detected */
.insertr-preview-content.insertr-fallback-style {
padding: var(--insertr-spacing-xs) var(--insertr-spacing-sm);
color: var(--insertr-text-primary);
}
/* Hover state for preview buttons - subtle visual feedback */
.insertr-style-btn.insertr-style-preview:hover {
background: var(--insertr-bg-secondary) !important;
border-color: var(--insertr-text-muted) !important;
transform: none !important;
border-color: var(--insertr-text-muted);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* Active state for preview buttons */
.insertr-style-btn.insertr-style-preview:active {
background: var(--insertr-border-color) !important;
transform: translateY(1px) !important;
transform: translateY(0);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
/* Editor components */

View File

@@ -88,43 +88,41 @@ export class StyleAwareEditor {
styles: detection.styles,
structure: detection.structure,
strategy: editingStrategy,
hasMultiPropertyElements: this.hasMultiPropertyElements(detection.structure)
hasMultiPropertyElements: false // Removed - not needed for HTML-first approach
};
}
/**
* Determine the best editing strategy based on content complexity
* Determine the best editing interface based on element type
* HTML-first approach: detect behavior from element tag, not attributes
*
* @param {Object} detection - Style detection results
* @returns {string} - Editing strategy: 'simple', 'rich', 'multi-property'
* @returns {string} - Editing interface: 'direct' or 'rich'
*/
determineEditingStrategy(detection) {
if (detection.structure.length === 0) {
return 'simple'; // No nested elements
const tagName = this.element.tagName.toLowerCase();
// Multi-property elements get direct editing interface
if (this.isMultiPropertyElement(tagName)) {
return 'direct';
}
const hasStyledElements = detection.structure.some(piece => piece.type === 'styled');
if (hasStyledElements) {
return 'rich'; // Rich text with styling (handles all styled content including links)
} else {
return 'simple'; // Plain text
}
// All other elements get rich HTML editing with style preservation
return 'rich';
}
/**
* Check if element is a multi-property element requiring direct editing
*
* @param {string} tagName - Element tag name
* @returns {boolean} - True if multi-property element
*/
isMultiPropertyElement(tagName) {
const multiPropertyTags = new Set(['a', 'button', 'img']);
return multiPropertyTags.has(tagName);
}
/**
* Check if structure contains multi-property elements
*
* @param {Array} structure - Content structure array
* @returns {boolean} - True if has multi-property elements
*/
hasMultiPropertyElements(structure) {
return structure.some(piece =>
piece.type === 'styled' &&
piece.properties &&
Object.keys(piece.properties).length > 1
);
}
/**
* Create editor interface based on analysis
@@ -138,16 +136,16 @@ export class StyleAwareEditor {
// Create appropriate editor based on strategy
switch (analysis.strategy) {
case 'simple':
this.createSimpleEditor();
case 'direct':
this.createDirectEditor(analysis);
break;
case 'rich':
this.createRichEditor(analysis);
break;
}
// Add toolbar if enabled and we have any styled content
if (this.options.showToolbar && (analysis.styles.size > 0 || analysis.hasMultiPropertyElements)) {
// Add toolbar if enabled and we have styled content (rich editor only)
if (this.options.showToolbar && analysis.strategy === 'rich' && analysis.styles.size > 0) {
this.createStyleToolbar(analysis.styles, analysis.structure);
}
@@ -156,17 +154,114 @@ export class StyleAwareEditor {
}
/**
* Create simple text editor for plain content
* Create direct property editor for multi-property elements (links, buttons, images)
*/
createSimpleEditor() {
const textarea = document.createElement('textarea');
textarea.className = 'insertr-simple-editor';
textarea.value = this.originalContent.text;
textarea.rows = 3;
textarea.placeholder = 'Enter content...';
createDirectEditor(analysis) {
const tagName = this.element.tagName.toLowerCase();
this.contentEditor = textarea;
this.editorContainer.appendChild(textarea);
if (tagName === 'a') {
this.createLinkEditor();
} else if (tagName === 'button') {
this.createButtonEditor();
} else if (tagName === 'img') {
this.createImageEditor();
} else {
// Fallback to rich editor
this.createRichEditor(analysis);
}
}
/**
* Create link editor with URL and text fields
*/
createLinkEditor() {
const form = document.createElement('div');
form.className = 'insertr-direct-editor insertr-link-editor';
// Text field
const textGroup = document.createElement('div');
textGroup.className = 'insertr-form-group';
textGroup.innerHTML = `
<label class="insertr-form-label">Link Text</label>
<input type="text" class="insertr-form-input" id="link-text" value="${this.element.textContent}" placeholder="Link text">
`;
// URL field
const urlGroup = document.createElement('div');
urlGroup.className = 'insertr-form-group';
urlGroup.innerHTML = `
<label class="insertr-form-label">URL</label>
<input type="url" class="insertr-form-input" id="link-url" value="${this.element.href || ''}" placeholder="https://example.com">
`;
// Target field
const targetGroup = document.createElement('div');
targetGroup.className = 'insertr-form-group';
targetGroup.innerHTML = `
<label class="insertr-form-label">Target</label>
<select class="insertr-form-select" id="link-target">
<option value="">Same window</option>
<option value="_blank" ${this.element.target === '_blank' ? 'selected' : ''}>New window</option>
</select>
`;
form.appendChild(textGroup);
form.appendChild(urlGroup);
form.appendChild(targetGroup);
this.contentEditor = form;
this.editorContainer.appendChild(form);
}
/**
* Create button editor with text field
*/
createButtonEditor() {
const form = document.createElement('div');
form.className = 'insertr-direct-editor insertr-button-editor';
// Text field
const textGroup = document.createElement('div');
textGroup.className = 'insertr-form-group';
textGroup.innerHTML = `
<label class="insertr-form-label">Button Text</label>
<input type="text" class="insertr-form-input" id="button-text" value="${this.element.textContent}" placeholder="Button text">
`;
form.appendChild(textGroup);
this.contentEditor = form;
this.editorContainer.appendChild(form);
}
/**
* Create image editor with src and alt fields
*/
createImageEditor() {
const form = document.createElement('div');
form.className = 'insertr-direct-editor insertr-image-editor';
// Source field
const srcGroup = document.createElement('div');
srcGroup.className = 'insertr-form-group';
srcGroup.innerHTML = `
<label class="insertr-form-label">Image URL</label>
<input type="url" class="insertr-form-input" id="image-src" value="${this.element.src || ''}" placeholder="https://example.com/image.jpg">
`;
// Alt text field
const altGroup = document.createElement('div');
altGroup.className = 'insertr-form-group';
altGroup.innerHTML = `
<label class="insertr-form-label">Alt Text</label>
<input type="text" class="insertr-form-input" id="image-alt" value="${this.element.alt || ''}" placeholder="Image description">
`;
form.appendChild(srcGroup);
form.appendChild(altGroup);
this.contentEditor = form;
this.editorContainer.appendChild(form);
}
/**
@@ -242,46 +337,31 @@ export class StyleAwareEditor {
const button = document.createElement('button');
button.type = 'button';
button.className = 'insertr-style-btn';
button.textContent = styleInfo.name;
button.title = `Apply ${styleInfo.name} style`;
button.dataset.styleId = styleId;
// Apply preview styling by copying computed styles from the original element
// Create preview content container
const previewContent = document.createElement('span');
previewContent.className = 'insertr-preview-content';
previewContent.textContent = styleInfo.name;
// Apply the original classes to the preview content (not the button)
if (styleInfo.element && styleInfo.classes && styleInfo.classes.length > 0) {
// Add the detected classes first
styleInfo.classes.forEach(className => {
button.classList.add(className);
});
// Add special button class
// Add special preview button class to the button
button.classList.add('insertr-style-preview');
// Copy specific style properties from the original element to ensure they show
const computedStyle = window.getComputedStyle(styleInfo.element);
// Copy color (most important for visual preview)
if (computedStyle.color && computedStyle.color !== 'rgb(0, 0, 0)') {
button.style.setProperty('color', computedStyle.color, 'important');
}
// Copy font-weight
if (computedStyle.fontWeight && computedStyle.fontWeight !== '400') {
button.style.setProperty('font-weight', computedStyle.fontWeight, 'important');
}
// Copy text-decoration (for underlines, etc.)
if (computedStyle.textDecoration && computedStyle.textDecoration !== 'none') {
button.style.setProperty('text-decoration', computedStyle.textDecoration, 'important');
}
// Copy text-transform (for uppercase, etc.)
if (computedStyle.textTransform && computedStyle.textTransform !== 'none') {
button.style.setProperty('text-transform', computedStyle.textTransform, 'important');
}
// Don't copy background-color to keep button appearance
// Add the detected classes to the preview content
styleInfo.classes.forEach(className => {
previewContent.classList.add(className);
});
} else {
// No meaningful styles detected - use fallback
previewContent.classList.add('insertr-fallback-style');
}
// Add the preview content to the button
button.appendChild(previewContent);
// Add click handler for style application
button.addEventListener('click', (e) => {
e.preventDefault();
@@ -449,18 +529,16 @@ export class StyleAwareEditor {
/**
* Extract current content from editor
* HTML-first approach: always return HTML content
*
* @returns {Object} - Extracted content
*/
extractContent() {
if (this.contentEditor.className.includes('simple-editor')) {
// Simple text editor
return {
type: 'text',
content: this.contentEditor.value
};
if (this.contentEditor.className.includes('direct-editor')) {
// Direct property editor - extract form values and generate HTML
return this.extractDirectEditorContent();
} else if (this.contentEditor.className.includes('rich-editor')) {
// Rich text editor
// Rich text editor - return HTML as-is
return {
type: 'html',
content: this.contentEditor.innerHTML
@@ -469,40 +547,74 @@ export class StyleAwareEditor {
return null;
}
/**
* Extract content from direct property editors
*
* @returns {Object} - Content with generated HTML
*/
extractDirectEditorContent() {
const tagName = this.element.tagName.toLowerCase();
if (tagName === 'a') {
const text = document.getElementById('link-text').value;
const url = document.getElementById('link-url').value;
const target = document.getElementById('link-target').value;
return {
type: 'html',
content: text,
properties: { href: url, target: target }
};
} else if (tagName === 'button') {
const text = document.getElementById('button-text').value;
return {
type: 'html',
content: text
};
} else if (tagName === 'img') {
const src = document.getElementById('image-src').value;
const alt = document.getElementById('image-alt').value;
return {
type: 'html',
content: '',
properties: { src: src, alt: alt }
};
}
return null;
}
/**
* Apply extracted content to the original element
* HTML-first approach: always use HTML preservation with optional property updates
*
* @param {Object} content - Content to apply
* @returns {boolean} - Success status
*/
applyContentToElement(content) {
try {
switch (content.type) {
case 'text':
// Simple text - just update textContent
this.element.textContent = content.content;
return true;
case 'html':
// Rich HTML - use HTML preservation engine
return this.htmlEngine.applyFromEditing(this.element, content.content);
case 'structured':
// Structured content - reconstruct using style engine
const reconstructedHTML = this.styleEngine.reconstructHTML(
content.structure,
this.detectedStyles,
content.updatedProperties
);
return this.htmlEngine.applyFromEditing(this.element, reconstructedHTML);
default:
console.error('Unknown content type:', content.type);
return false;
// Apply properties if specified (for direct editors)
if (content.properties) {
for (const [property, value] of Object.entries(content.properties)) {
if (value) {
this.element.setAttribute(property, value);
} else {
this.element.removeAttribute(property);
}
}
}
// Apply HTML content using preservation engine
if (content.content !== undefined) {
return this.htmlEngine.applyFromEditing(this.element, content.content);
}
return true;
} catch (error) {
console.error('Failed to apply content:', error);
return false;

View File

@@ -1,37 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Syntactic Sugar Test</title>
</head>
<body>
<h1>Syntactic Sugar Transformation Test</h1>
<!-- Test 1: Container with .insertr should be transformed -->
<section class="hero insertr">
<h1>Hero Title</h1>
<p>Hero description text</p>
<button>Call to Action</button>
</section>
<!-- Test 2: Individual element with .insertr should remain -->
<h2 class="insertr">Individual Header</h2>
<!-- Test 3: Container without .insertr should get expansion if viable -->
<div class="card">
<h3>Card Title</h3>
<p>Card description</p>
</div>
<!-- Test 4: Complex container with mixed content -->
<article class="blog-post insertr">
<header>
<h1>Blog Post Title</h1>
<time>2024-01-01</time>
</header>
<p>First paragraph of content</p>
<p>Second paragraph with <strong>formatting</strong></p>
</article>
</body>
</html>

View File

@@ -1,162 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Multi-Property Elements Test</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 2rem;
line-height: 1.6;
}
.test-output {
background: #f5f5f5;
padding: 1rem;
border-radius: 8px;
font-family: 'Monaco', 'Courier New', monospace;
white-space: pre-wrap;
max-height: 600px;
overflow-y: auto;
}
.fancy { color: #7c3aed; text-decoration: none; border-bottom: 2px solid #a855f7; }
.btn { background: #3b82f6; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; }
</style>
</head>
<body>
<h1>🧪 Multi-Property Elements Test</h1>
<p>Testing our enhanced system that handles elements with multiple editable properties like links (href + content).</p>
<div id="test-output" class="test-output">Loading...</div>
<!-- Test elements with multiple editable properties -->
<div style="display: none;">
<p id="link-example" class="insertr">Visit our <a class="fancy" href="https://example.com" title="Example Site" target="_blank">about page</a> for more info.</p>
<p id="button-example" class="insertr">Ready to start? <button class="btn" type="button" data-action="signup">Sign Up Now</button> and begin!</p>
<div id="image-example" class="insertr">Check out this image: <img src="https://via.placeholder.com/150" alt="Placeholder" title="Sample Image" class="responsive"></div>
</div>
<script type="module">
import { styleDetectionEngine } from './lib/src/utils/style-detection.js';
const output = document.getElementById('test-output');
function runMultiPropertyTests() {
let results = '';
results += '🔍 TESTING MULTI-PROPERTY ELEMENTS\n';
results += '==================================\n\n';
// Test Link Element with href + content + title + target
results += testMultiPropertyElement('link-example', 'Link with Multiple Properties');
// Test Button Element with content + data attributes
results += testMultiPropertyElement('button-example', 'Button with Data Attributes');
// Test Image Element with src + alt + title
results += testMultiPropertyElement('image-example', 'Image with Multiple Attributes');
output.textContent = results;
}
function testMultiPropertyElement(elementId, testName) {
let result = `📝 ${testName}\n`;
const element = document.getElementById(elementId);
if (!element) {
return result + `❌ Element ${elementId} not found\n\n`;
}
result += `Original HTML: ${element.innerHTML}\n`;
try {
// Test our enhanced structure detection
const detection = styleDetectionEngine.detectStylesAndStructure(element);
result += `\n🎨 Detected Styles (${detection.styles.size}):\n`;
for (const [id, style] of detection.styles) {
result += `${style.name} (${id})\n`;
result += ` Template: ${JSON.stringify(style.template, null, 4)}\n`;
}
result += `\n📍 Content Structure (${detection.structure.length} pieces):\n`;
detection.structure.forEach((piece, index) => {
if (piece.type === 'text') {
result += ` ${index}: TEXT: "${piece.content}"\n`;
} else if (piece.type === 'styled') {
result += ` ${index}: STYLED: ${piece.styleId}\n`;
result += ` Properties: ${JSON.stringify(piece.properties, null, 6)}\n`;
}
});
// Test property extraction for complex elements
const styledPieces = detection.structure.filter(p => p.type === 'styled');
if (styledPieces.length > 0) {
result += `\n🔧 Multi-Property Analysis:\n`;
styledPieces.forEach((piece, index) => {
const style = detection.styles.get(piece.styleId);
result += ` Element ${index + 1}: ${style.tagName}\n`;
result += ` Editable Properties: ${style.template.editableProperties?.join(', ') || 'content only'}\n`;
result += ` Current Values:\n`;
Object.entries(piece.properties).forEach(([key, value]) => {
result += ` ${key}: "${value}"\n`;
});
});
}
// Test reconstruction with modified properties
result += `\n🔄 Testing Property Modification:\n`;
const modifiedProperties = {};
if (styledPieces.length > 0) {
const firstStyled = detection.structure.findIndex(p => p.type === 'styled');
const originalProps = detection.structure[firstStyled].properties;
// Create modified properties for testing
const newProps = { ...originalProps };
if (newProps.content) newProps.content = 'MODIFIED TEXT';
if (newProps.href) newProps.href = 'https://modified.com';
if (newProps.title) newProps.title = 'Modified Title';
if (newProps.alt) newProps.alt = 'Modified Alt Text';
modifiedProperties[firstStyled] = { properties: newProps };
result += ` Original Properties: ${JSON.stringify(originalProps, null, 4)}\n`;
result += ` Modified Properties: ${JSON.stringify(newProps, null, 4)}\n`;
}
// Test reconstruction
const reconstructed = styleDetectionEngine.reconstructHTML(detection.structure, detection.styles, modifiedProperties);
result += `\n🔧 Reconstructed HTML: ${reconstructed}\n`;
// Test plain text extraction
const plainText = styleDetectionEngine.extractTextFromStructure(detection.structure);
result += `📝 Plain Text: "${plainText}"\n`;
// Verify structure preservation
const originalText = element.textContent;
const preservesText = plainText === originalText;
result += `\n✅ Checks:\n`;
result += ` Text preservation: ${preservesText ? '✅' : '❌'}\n`;
result += ` Multi-property support: ${styledPieces.some(p => Object.keys(p.properties).length > 1) ? '✅' : '❌'}\n`;
result += ` Template complexity: ${detection.styles.size > 0 && Array.from(detection.styles.values()).some(s => s.template.editableProperties?.length > 1) ? '✅' : '❌'}\n`;
if (!preservesText) {
result += ` Expected text: "${originalText}"\n`;
result += ` Got text: "${plainText}"\n`;
}
} catch (error) {
result += `❌ Error: ${error.message}\n`;
result += `Stack: ${error.stack}\n`;
}
return result + '\n';
}
// Run tests when page loads
runMultiPropertyTests();
</script>
</body>
</html>

View File

@@ -1,134 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Structure Preservation Test</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 2rem;
line-height: 1.6;
}
.test-output {
background: #f5f5f5;
padding: 1rem;
border-radius: 8px;
font-family: 'Monaco', 'Courier New', monospace;
white-space: pre-wrap;
max-height: 500px;
overflow-y: auto;
}
.emph { color: #dc2626; font-weight: 700; }
.fancy { color: #7c3aed; text-decoration: none; border-bottom: 2px solid #a855f7; }
.highlight { background: #fef08a; padding: 0 0.25rem; border-radius: 3px; }
.brand { color: #059669; font-weight: 800; text-transform: uppercase; }
</style>
</head>
<body>
<h1>🧪 Structure Preservation Test</h1>
<p>Testing if our new system preserves WHERE styles are positioned, not just WHAT styles exist.</p>
<div id="test-output" class="test-output">Loading...</div>
<!-- Test elements -->
<div style="display: none;">
<p id="example1" class="insertr">Hello <strong class="emph">world</strong> and welcome!</p>
<p id="example2" class="insertr">Visit our <a class="fancy" href="#about">about page</a> for more info.</p>
<p id="example3" class="insertr">Save up to <strong class="highlight">50%</strong> today only!</p>
</div>
<script type="module">
import { styleDetectionEngine } from './lib/src/utils/style-detection.js';
const output = document.getElementById('test-output');
function runStructureTests() {
let results = '';
results += '🔍 TESTING STRUCTURE PRESERVATION\n';
results += '================================\n\n';
// Test Example 1: <p>Hello <strong class="emph">world</strong> and welcome!</p>
results += testStructurePreservation('example1', 'Example 1: Styled Strong Element');
// Test Example 2: <p>Visit our <a class="fancy" href="#about">about page</a> for more info.</p>
results += testStructurePreservation('example2', 'Example 2: Styled Link Element');
// Test Example 3: <p>Save up to <strong class="highlight">50%</strong> today only!</p>
results += testStructurePreservation('example3', 'Example 3: Highlighted Text');
output.textContent = results;
}
function testStructurePreservation(elementId, testName) {
let result = `📝 ${testName}\n`;
const element = document.getElementById(elementId);
if (!element) {
return result + `❌ Element ${elementId} not found\n\n`;
}
result += `Original HTML: ${element.innerHTML}\n`;
try {
// Test our new structure-preserving detection
const detection = styleDetectionEngine.detectStylesAndStructure(element);
result += `\n🎨 Detected Styles (${detection.styles.size}):\n`;
for (const [id, style] of detection.styles) {
result += `${style.name} (${id})\n`;
}
result += `\n📍 Content Structure (${detection.structure.length} pieces):\n`;
detection.structure.forEach((piece, index) => {
if (piece.type === 'text') {
result += ` ${index}: TEXT: "${piece.content}"\n`;
} else if (piece.type === 'styled') {
result += ` ${index}: STYLED: "${piece.content}" (${piece.styleId})\n`;
}
});
// Test reconstruction
const reconstructed = styleDetectionEngine.reconstructHTML(detection.structure, detection.styles);
result += `\n🔄 Reconstructed HTML: ${reconstructed}\n`;
// Test plain text extraction
const plainText = styleDetectionEngine.extractTextFromStructure(detection.structure);
result += `📝 Plain Text: "${plainText}"\n`;
// Verify structure preservation
const originalText = element.textContent;
const preservesText = plainText === originalText;
const preservesHTML = reconstructed === element.innerHTML;
result += `\n✅ Checks:\n`;
result += ` Text preservation: ${preservesText ? '✅' : '❌'}\n`;
result += ` HTML reconstruction: ${preservesHTML ? '✅' : '❌'}\n`;
if (preservesText && preservesHTML) {
result += ` 🎉 PERFECT STRUCTURE PRESERVATION!\n`;
} else {
result += ` ⚠️ Structure not perfectly preserved\n`;
if (!preservesText) {
result += ` Expected text: "${originalText}"\n`;
result += ` Got text: "${plainText}"\n`;
}
if (!preservesHTML) {
result += ` Expected HTML: ${element.innerHTML}\n`;
result += ` Got HTML: ${reconstructed}\n`;
}
}
} catch (error) {
result += `❌ Error: ${error.message}\n`;
}
return result + '\n';
}
// Run tests when page loads
runStructureTests();
</script>
</body>
</html>

View File

@@ -1,174 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Style Preservation System Test</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
margin: 2rem;
line-height: 1.6;
}
.test-output {
background: #f5f5f5;
padding: 1rem;
border-radius: 8px;
font-family: 'Monaco', 'Courier New', monospace;
white-space: pre-wrap;
max-height: 500px;
overflow-y: auto;
}
.test-controls {
margin: 1rem 0;
}
button {
background: #007acc;
color: white;
border: none;
padding: 0.5rem 1rem;
border-radius: 4px;
cursor: pointer;
margin-right: 0.5rem;
}
button:hover {
background: #005a9e;
}
.emph { color: #dc2626; font-weight: 700; }
.fancy { color: #7c3aed; text-decoration: none; border-bottom: 2px solid #a855f7; }
.highlight { background: #fef08a; padding: 0 0.25rem; border-radius: 3px; }
.brand { color: #059669; font-weight: 800; text-transform: uppercase; }
.btn { background: #3b82f6; color: white; border: none; padding: 0.5rem 1rem; border-radius: 6px; }
</style>
</head>
<body>
<h1>🧪 Style Preservation System Test</h1>
<p>This page tests our new StyleDetectionEngine and HTMLPreservationEngine with actual DOM elements.</p>
<div class="test-controls">
<button onclick="runStyleTests()">Test Style Detection</button>
<button onclick="runHTMLTests()">Test HTML Preservation</button>
<button onclick="runAllTests()">Run All Tests</button>
<button onclick="clearOutput()">Clear Output</button>
</div>
<div id="test-output" class="test-output">Click a test button to run tests...</div>
<!-- Sample elements for testing (hidden) -->
<div style="display: none;">
<p id="example1" class="insertr">Hello <strong class="emph">world</strong> and welcome!</p>
<p id="example2" class="insertr">Visit our <a class="fancy" href="#about">about page</a> for more info.</p>
<p id="example4" class="insertr">Welcome to <strong class="brand">Acme Corp</strong> where we create <span class="highlight">innovative</span> solutions for <em class="emph">modern</em> businesses.</p>
<p id="example5" class="insertr">Ready to start? <button class="btn" data-action="signup" data-analytics="cta-main">Sign Up Now</button> and begin your journey!</p>
</div>
<script type="module">
// Import our style detection system
import { styleDetectionEngine } from './lib/src/utils/style-detection.js';
import { htmlPreservationEngine } from './lib/src/utils/html-preservation.js';
import { runStyleDetectionTests, runHTMLPreservationTests, runAllTests as runTestSuite } from './lib/src/utils/test-runner.js';
// Make functions available globally
window.styleDetectionEngine = styleDetectionEngine;
window.htmlPreservationEngine = htmlPreservationEngine;
// Store original console.log
const originalLog = console.log;
const output = document.getElementById('test-output');
// Override console.log to display in our output div
function captureOutput(callback) {
const logs = [];
console.log = (...args) => {
logs.push(args.join(' '));
originalLog(...args);
};
try {
const result = callback();
output.textContent = logs.join('\n');
return result;
} finally {
console.log = originalLog;
}
}
window.runStyleTests = () => {
captureOutput(() => runStyleDetectionTests());
};
window.runHTMLTests = () => {
captureOutput(() => runHTMLPreservationTests());
};
window.runAllTests = () => {
captureOutput(() => runTestSuite());
};
window.clearOutput = () => {
output.textContent = 'Output cleared. Click a test button to run tests...';
};
// Manual testing functions
window.testStyleDetection = (elementId) => {
const element = document.getElementById(elementId);
if (!element) {
console.log(`Element ${elementId} not found`);
return;
}
console.log(`\n🔍 Testing element: ${elementId}`);
console.log(`HTML: ${element.outerHTML}`);
const styles = styleDetectionEngine.detectStyles(element);
console.log(`\nDetected ${styles.size} styles:`);
for (const [id, style] of styles) {
console.log(`${style.name} (${style.tagName}.${style.classes.join('.')})`);
console.log(` Template: ${style.template}`);
if (Object.keys(style.attributes).length > 0) {
console.log(` Attributes: ${JSON.stringify(style.attributes)}`);
}
}
};
window.testHTMLPreservation = (elementId) => {
const element = document.getElementById(elementId);
if (!element) {
console.log(`Element ${elementId} not found`);
return;
}
console.log(`\n🔧 Testing HTML preservation: ${elementId}`);
console.log(`Original: ${element.outerHTML}`);
// Extract content
const extracted = htmlPreservationEngine.extractForEditing(element);
console.log(`\nExtracted HTML: ${extracted.html}`);
console.log(`Extracted Text: ${extracted.text}`);
console.log(`Container Attributes: ${JSON.stringify(extracted.containerAttributes)}`);
console.log(`Has Nested Elements: ${extracted.hasNestedElements}`);
// Test applying content
const testContent = 'Modified <strong class="emph">content</strong> for testing!';
const success = htmlPreservationEngine.applyFromEditing(element, testContent);
console.log(`\nApplication Success: ${success}`);
console.log(`Updated HTML: ${element.outerHTML}`);
};
// Show initial instructions
output.textContent = `🧪 Style Preservation System Test Ready!
Available manual tests:
• testStyleDetection('example1') - Test style detection on Example 1
• testStyleDetection('example2') - Test style detection on Example 2
• testHTMLPreservation('example1') - Test HTML preservation
• runStyleTests() - Run all style detection tests
• runHTMLTests() - Run all HTML preservation tests
• runAllTests() - Run complete test suite
Click the buttons above or open browser console and run these commands manually.`;
</script>
</body>
</html>