feat: Implement complete style detection and preservation foundation

- Add StyleDetectionEngine with one-layer-deep nested element analysis
- Add HTMLPreservationEngine for direct HTML manipulation without lossy conversion
- Implement structure-preserving content parsing that maintains element positions
- Add multi-property element support for links (href + content), images (src + alt), buttons
- Create comprehensive test suite with real DOM element validation
- Replace markdown-based system foundation with HTML-first architecture
- Preserve all element attributes (classes, IDs, data-*, aria-*) during editing
- Generate human-readable style names from detected nested elements
- Support template extraction with multiple insertion points for complex elements

Foundation complete for Phase 2 style-aware editor interface per CLASSES.md specification.
This commit is contained in:
2025-09-19 19:33:56 +02:00
parent 968e64a57e
commit 67f9f242b5
6 changed files with 2032 additions and 3 deletions

View File

@@ -0,0 +1,337 @@
/**
* Simple test runner for style preservation system
* Tests our implementation with actual DOM elements
*/
import { styleDetectionEngine } from './style-detection.js';
import { htmlPreservationEngine } from './html-preservation.js';
/**
* Run all style detection tests with real DOM elements
*/
export function runStyleDetectionTests() {
console.log('🧪 Running Style Detection Tests');
console.log('================================');
const results = [];
// Test 1: Demo Example 1 - Styled Strong Element
results.push(testExample1());
// Test 2: Demo Example 2 - Styled Link Element
results.push(testExample2());
// Test 3: Demo Example 4 - Multiple Styled Elements
results.push(testExample4());
// Test 4: Demo Example 5 - Button with Data Attributes
results.push(testExample5());
// Summary
const passed = results.filter(r => r.passed).length;
const total = results.length;
console.log(`\n📊 Test Results: ${passed}/${total} passed`);
if (passed === total) {
console.log('✅ All style detection tests passed!');
} else {
console.log('❌ Some tests failed - see details above');
}
return { passed, total, results };
}
/**
* Test Example 1: <p>Hello <strong class="emph">world</strong> and welcome!</p>
*/
function testExample1() {
console.log('\n🔍 Test 1: Styled Strong Element');
try {
// Create test element
const container = document.createElement('p');
container.className = 'insertr';
container.innerHTML = 'Hello <strong class="emph">world</strong> and welcome!';
// Detect styles
const detectedStyles = styleDetectionEngine.detectStyles(container);
// Validate results
const hasEmphStyle = detectedStyles.has('strong_emph');
const emphStyle = detectedStyles.get('strong_emph');
const correctName = emphStyle?.name === 'Emphasis';
const correctTag = emphStyle?.tagName === 'strong';
const correctClasses = emphStyle?.classes?.includes('emph');
const passed = hasEmphStyle && correctName && correctTag && correctClasses;
console.log(` Detected ${detectedStyles.size} style(s)`);
console.log(` Found "Emphasis" style: ${hasEmphStyle ? '✅' : '❌'}`);
console.log(` Correct name: ${correctName ? '✅' : '❌'}`);
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
return { test: 'Example 1', passed, details: { hasEmphStyle, correctName, correctTag, correctClasses } };
} catch (error) {
console.log(` Error: ${error.message}`);
console.log(` Result: ❌ FAILED`);
return { test: 'Example 1', passed: false, error: error.message };
}
}
/**
* Test Example 2: <p>Visit our <a class="fancy" href="#about">about page</a> for more info.</p>
*/
function testExample2() {
console.log('\n🔍 Test 2: Styled Link Element');
try {
const container = document.createElement('p');
container.className = 'insertr';
container.innerHTML = 'Visit our <a class="fancy" href="#about">about page</a> for more info.';
const detectedStyles = styleDetectionEngine.detectStyles(container);
const hasFancyStyle = detectedStyles.has('a_fancy');
const fancyStyle = detectedStyles.get('a_fancy');
const correctName = fancyStyle?.name === 'Fancy Link';
const hasHref = fancyStyle?.attributes?.href === '#about';
const passed = hasFancyStyle && correctName && hasHref;
console.log(` Detected ${detectedStyles.size} style(s)`);
console.log(` Found "Fancy Link" style: ${hasFancyStyle ? '✅' : '❌'}`);
console.log(` Preserved href attribute: ${hasHref ? '✅' : '❌'}`);
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
return { test: 'Example 2', passed, details: { hasFancyStyle, correctName, hasHref } };
} catch (error) {
console.log(` Error: ${error.message}`);
console.log(` Result: ❌ FAILED`);
return { test: 'Example 2', passed: false, error: error.message };
}
}
/**
* Test Example 4: Multiple styled elements
*/
function testExample4() {
console.log('\n🔍 Test 4: Multiple Styled Elements');
try {
const container = document.createElement('p');
container.className = 'insertr';
container.innerHTML = 'Welcome to <strong class="brand">Acme Corp</strong> where we create <span class="highlight">innovative</span> solutions for <em class="emph">modern</em> businesses.';
const detectedStyles = styleDetectionEngine.detectStyles(container);
const hasBrand = detectedStyles.has('strong_brand');
const hasHighlight = detectedStyles.has('span_highlight');
const hasEmphItalic = detectedStyles.has('em_emph');
const correctCount = detectedStyles.size === 3;
const passed = hasBrand && hasHighlight && hasEmphItalic && correctCount;
console.log(` Detected ${detectedStyles.size} style(s) (expected 3)`);
console.log(` Found "Brand" style: ${hasBrand ? '✅' : '❌'}`);
console.log(` Found "Highlight" style: ${hasHighlight ? '✅' : '❌'}`);
console.log(` Found "Emphasis Italic" style: ${hasEmphItalic ? '✅' : '❌'}`);
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
return { test: 'Example 4', passed, details: { hasBrand, hasHighlight, hasEmphItalic, correctCount } };
} catch (error) {
console.log(` Error: ${error.message}`);
console.log(` Result: ❌ FAILED`);
return { test: 'Example 4', passed: false, error: error.message };
}
}
/**
* Test Example 5: Button with data attributes
*/
function testExample5() {
console.log('\n🔍 Test 5: Button with Data Attributes');
try {
const container = document.createElement('p');
container.className = 'insertr';
container.innerHTML = 'Ready to start? <button class="btn" data-action="signup" data-analytics="cta-main">Sign Up Now</button> and begin your journey!';
const detectedStyles = styleDetectionEngine.detectStyles(container);
const hasButtonStyle = detectedStyles.has('button_btn');
const buttonStyle = detectedStyles.get('button_btn');
const correctName = buttonStyle?.name === 'Button Style';
const hasDataAction = buttonStyle?.attributes?.['data-action'] === 'signup';
const hasDataAnalytics = buttonStyle?.attributes?.['data-analytics'] === 'cta-main';
const passed = hasButtonStyle && correctName && hasDataAction && hasDataAnalytics;
console.log(` Detected ${detectedStyles.size} style(s)`);
console.log(` Found "Button Style": ${hasButtonStyle ? '✅' : '❌'}`);
console.log(` Preserved data-action: ${hasDataAction ? '✅' : '❌'}`);
console.log(` Preserved data-analytics: ${hasDataAnalytics ? '✅' : '❌'}`);
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
return { test: 'Example 5', passed, details: { hasButtonStyle, correctName, hasDataAction, hasDataAnalytics } };
} catch (error) {
console.log(` Error: ${error.message}`);
console.log(` Result: ❌ FAILED`);
return { test: 'Example 5', passed: false, error: error.message };
}
}
/**
* Run HTML preservation tests
*/
export function runHTMLPreservationTests() {
console.log('\n🧪 Running HTML Preservation Tests');
console.log('==================================');
const results = [];
results.push(testHTMLExtraction());
results.push(testHTMLApplication());
results.push(testAttributePreservation());
const passed = results.filter(r => r.passed).length;
const total = results.length;
console.log(`\n📊 HTML Preservation Results: ${passed}/${total} passed`);
return { passed, total, results };
}
function testHTMLExtraction() {
console.log('\n🔍 HTML Extraction Test');
try {
const element = document.createElement('p');
element.className = 'insertr test';
element.id = 'test-element';
element.innerHTML = 'Hello <strong class="emph">world</strong>!';
const extracted = htmlPreservationEngine.extractForEditing(element);
const hasHTML = extracted.html === 'Hello <strong class="emph">world</strong>!';
const hasText = extracted.text === 'Hello world!';
const hasAttributes = extracted.containerAttributes.class === 'insertr test';
const detectsNesting = extracted.hasNestedElements === true;
const passed = hasHTML && hasText && hasAttributes && detectsNesting;
console.log(` HTML extraction: ${hasHTML ? '✅' : '❌'}`);
console.log(` Text extraction: ${hasText ? '✅' : '❌'}`);
console.log(` Attribute preservation: ${hasAttributes ? '✅' : '❌'}`);
console.log(` Nesting detection: ${detectsNesting ? '✅' : '❌'}`);
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
return { test: 'HTML Extraction', passed };
} catch (error) {
console.log(` Error: ${error.message}`);
console.log(` Result: ❌ FAILED`);
return { test: 'HTML Extraction', passed: false, error: error.message };
}
}
function testHTMLApplication() {
console.log('\n🔍 HTML Application Test');
try {
const element = document.createElement('p');
element.className = 'insertr original';
const newHTML = 'Updated <strong class="emph">content</strong>!';
const success = htmlPreservationEngine.applyFromEditing(element, newHTML);
const appliedCorrectly = element.innerHTML === newHTML;
const preservedClass = element.className === 'insertr original';
const passed = success && appliedCorrectly && preservedClass;
console.log(` Application success: ${success ? '✅' : '❌'}`);
console.log(` Content applied: ${appliedCorrectly ? '✅' : '❌'}`);
console.log(` Container class preserved: ${preservedClass ? '✅' : '❌'}`);
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
return { test: 'HTML Application', passed };
} catch (error) {
console.log(` Error: ${error.message}`);
console.log(` Result: ❌ FAILED`);
return { test: 'HTML Application', passed: false, error: error.message };
}
}
function testAttributePreservation() {
console.log('\n🔍 Attribute Preservation Test');
try {
const element = document.createElement('div');
const originalAttrs = {
class: 'insertr test',
id: 'unique-id',
'data-value': '123'
};
// Apply original attributes
Object.entries(originalAttrs).forEach(([key, value]) => {
element.setAttribute(key, value);
});
// Extract and restore
const extracted = htmlPreservationEngine.extractElementAttributes(element);
const newElement = document.createElement('div');
htmlPreservationEngine.restoreElementAttributes(newElement, extracted);
const classRestored = newElement.getAttribute('class') === 'insertr test';
const idRestored = newElement.getAttribute('id') === 'unique-id';
const dataRestored = newElement.getAttribute('data-value') === '123';
const passed = classRestored && idRestored && dataRestored;
console.log(` Class attribute: ${classRestored ? '✅' : '❌'}`);
console.log(` ID attribute: ${idRestored ? '✅' : '❌'}`);
console.log(` Data attribute: ${dataRestored ? '✅' : '❌'}`);
console.log(` Result: ${passed ? '✅ PASSED' : '❌ FAILED'}`);
return { test: 'Attribute Preservation', passed };
} catch (error) {
console.log(` Error: ${error.message}`);
console.log(` Result: ❌ FAILED`);
return { test: 'Attribute Preservation', passed: false, error: error.message };
}
}
/**
* Run all tests
*/
export function runAllTests() {
console.log('🚀 Running All Style Preservation System Tests');
console.log('==============================================');
const styleResults = runStyleDetectionTests();
const htmlResults = runHTMLPreservationTests();
const totalPassed = styleResults.passed + htmlResults.passed;
const totalTests = styleResults.total + htmlResults.total;
console.log(`\n🎯 Overall Results: ${totalPassed}/${totalTests} tests passed`);
if (totalPassed === totalTests) {
console.log('🎉 All tests passed! Style preservation system is working correctly.');
} else {
console.log('⚠️ Some tests failed. Review implementation before proceeding.');
}
return {
passed: totalPassed === totalTests,
details: { styleResults, htmlResults }
};
}