.. _ckeditor-conversions: ================================ CKEditor Conversion System ================================ Complete guide to the upcast/downcast conversion system for transforming between HTML and model representations. Conversion Architecture ======================= Three-Layer System ------------------ :: ┌──────────────────────────────────────┐ │ Data Layer (Database/API) │ │ HTML with data-* attributes │ │ │ └────────────┬─────────────────────────┘ │ │ Upcast (Load) │ ┌────────────▼─────────────────────────┐ │ Model Layer (Abstract) │ │ typo3image element with attributes │ │ { name: 'typo3image', fileUid: 123 } │ └────────────┬─────────────────────────┘ │ │ Downcast (Render) │ ┌────────────▼─────────────────────────┐ │ View Layer (Editor Display) │ │ Visual HTML in contenteditable │ │ │ └──────────────────────────────────────┘ Upcast Conversions ================== Purpose ------- **Upcast**: Transforms HTML (from database/API) into model elements when loading content into editor **When Used**: - Initial content load into CKEditor - Paste from clipboard - Insert HTML programmatically Image Element Upcast -------------------- .. code-block:: javascript editor.conversion.for('upcast').elementToElement({ view: { name: 'img', attributes: ['data-htmlarea-file-uid', 'src'] }, model: (viewElement, { writer }) => { return writer.createElement('typo3image', { fileUid: viewElement.getAttribute('data-htmlarea-file-uid'), fileTable: viewElement.getAttribute('data-htmlarea-file-table') || 'sys_file', src: viewElement.getAttribute('src'), width: viewElement.getAttribute('width') || '', height: viewElement.getAttribute('height') || '', class: viewElement.getAttribute('class') || '', alt: viewElement.getAttribute('alt') || '', altOverride: viewElement.getAttribute('data-alt-override') || false, title: viewElement.getAttribute('title') || '', titleOverride: viewElement.getAttribute('data-title-override') || false, enableZoom: viewElement.getAttribute('data-htmlarea-zoom') || false, }); } }); Configuration Breakdown ----------------------- View Matcher ^^^^^^^^^^^^ .. code-block:: javascript view: { name: 'img', attributes: ['data-htmlarea-file-uid', 'src'] } **Purpose**: Defines which HTML elements should be converted **Matching Logic**: - **Element name**: Must be ```` - **Required attributes**: Must have both ``data-htmlarea-file-uid`` AND ``src`` **Examples**: Matched (will be upcasted): .. code-block:: html Not matched (regular img passthrough): .. code-block:: html Model Creator Function ^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: javascript model: (viewElement, { writer }) => { return writer.createElement('typo3image', { // attributes... }); } **Parameters**: - ``viewElement``: The matched ```` element from HTML - ``writer``: Model writer for creating elements **Return**: New model element with extracted attributes Attribute Extraction ^^^^^^^^^^^^^^^^^^^^ .. code-block:: javascript fileUid: viewElement.getAttribute('data-htmlarea-file-uid'), **Pattern**: Extract HTML attribute → map to model attribute **Mappings**: =========================== =================== ======================== HTML Attribute Model Attribute Transformation =========================== =================== ======================== data-htmlarea-file-uid fileUid Direct copy data-htmlarea-file-table fileTable Default: 'sys_file' src src Direct copy width width Default: empty string height height Default: empty string class class Default: empty string alt alt Default: empty string data-alt-override altOverride Default: false title title Default: empty string data-title-override titleOverride Default: false data-htmlarea-zoom enableZoom Default: false =========================== =================== ======================== Upcast Example Flow ------------------- **Input HTML**: .. code-block:: html Product photo **Upcast Process**: 1. CKEditor parser encounters ```` element 2. Checks if has ``data-htmlarea-file-uid`` and ``src`` 3. Calls model creator function 4. Extracts all attributes 5. Creates model element **Result Model Element**: .. code-block:: javascript { name: 'typo3image', attributes: { fileUid: 123, fileTable: 'sys_file', src: '/fileadmin/image.jpg', width: '800', height: '600', alt: 'Product photo', altOverride: false, title: 'Click to enlarge', titleOverride: false, class: 'img-fluid', enableZoom: true } } Downcast Conversions ==================== Purpose ------- **Downcast**: Transforms model elements into HTML for editor display and data saving **Two Pipelines**: 1. **Editing Downcast**: Render in contenteditable (editor view) 2. **Data Downcast**: Serialize for database storage Image Element Downcast ---------------------- .. code-block:: javascript editor.conversion.for('downcast').elementToElement({ model: { name: 'typo3image', attributes: ['fileUid', 'fileTable', 'src'] }, view: (modelElement, { writer }) => { const attributes = { 'src': modelElement.getAttribute('src'), 'data-htmlarea-file-uid': modelElement.getAttribute('fileUid'), 'data-htmlarea-file-table': modelElement.getAttribute('fileTable'), 'width': modelElement.getAttribute('width'), 'height': modelElement.getAttribute('height'), 'class': modelElement.getAttribute('class') || '', 'title': modelElement.getAttribute('title') || '', 'alt': modelElement.getAttribute('alt') || '', }; if (modelElement.getAttribute('titleOverride')) { attributes['data-title-override'] = true; } if (modelElement.getAttribute('altOverride')) { attributes['data-alt-override'] = true; } if (modelElement.getAttribute('enableZoom')) { attributes['data-htmlarea-zoom'] = true; } return writer.createEmptyElement('img', attributes); }, }); Configuration Breakdown ----------------------- Model Matcher ^^^^^^^^^^^^^ .. code-block:: javascript model: { name: 'typo3image', attributes: ['fileUid', 'fileTable', 'src'] } **Purpose**: Defines which model elements trigger this conversion **Matching**: - Element name is ``typo3image`` - Has ``fileUid``, ``fileTable``, ``src`` attributes (required for meaningful output) View Creator Function ^^^^^^^^^^^^^^^^^^^^^ .. code-block:: javascript view: (modelElement, { writer }) => { return writer.createEmptyElement('img', attributes); } **Parameters**: - ``modelElement``: The typo3image model element - ``writer``: View writer for creating elements **Return**: New view element (````) Attribute Mapping ^^^^^^^^^^^^^^^^^ .. code-block:: javascript const attributes = { 'src': modelElement.getAttribute('src'), 'data-htmlarea-file-uid': modelElement.getAttribute('fileUid'), // ... }; **Pattern**: Read model attribute → map to HTML attribute **Reverse Mappings**: =================== =========================== ======================== Model Attribute HTML Attribute Transformation =================== =========================== ======================== src src Direct copy fileUid data-htmlarea-file-uid Direct copy fileTable data-htmlarea-file-table Direct copy width width Direct copy height height Direct copy class class Default: empty string alt alt Default: empty string title title Default: empty string altOverride data-alt-override Only if true titleOverride data-title-override Only if true enableZoom data-htmlarea-zoom Only if true =================== =========================== ======================== Conditional Attributes ^^^^^^^^^^^^^^^^^^^^^^ .. code-block:: javascript if (modelElement.getAttribute('titleOverride')) { attributes['data-title-override'] = true; } **Pattern**: Only add boolean attributes when true **Why**: Cleaner HTML output, avoid unnecessary attributes **Result**: .. code-block:: html Downcast Example Flow --------------------- **Input Model Element**: .. code-block:: javascript { name: 'typo3image', attributes: { fileUid: 123, fileTable: 'sys_file', src: '/fileadmin/image.jpg', width: '800', height: '600', alt: 'Product photo', altOverride: true, class: 'img-fluid', enableZoom: true } } **Downcast Process**: 1. CKEditor needs to render model element 2. Finds typo3image → elementToElement converter 3. Calls view creator function 4. Maps all attributes 5. Adds conditional attributes 6. Creates view element **Result HTML**: .. code-block:: html Product photo Attribute Converters ==================== Class Attribute Converter -------------------------- .. code-block:: javascript editor.conversion.for('downcast').attributeToAttribute({ model: { name: 'typo3image', key: 'class' }, view: 'class' }); **Purpose**: Immediately sync class attribute changes to view .. note:: The elementToElement converter only runs on element creation. This converter handles attribute updates. **Behavior**: .. code-block:: javascript // User changes class via style system editor.model.change(writer => { writer.setAttribute('class', 'float-left mr-3', modelElement); }); // Immediately reflected in view Custom Attribute Converters ---------------------------- You can add converters for any attribute: .. code-block:: javascript // Width changes immediately visible editor.conversion.for('downcast').attributeToAttribute({ model: { name: 'typo3image', key: 'width' }, view: 'width' }); // Alt changes immediately visible editor.conversion.for('downcast').attributeToAttribute({ model: { name: 'typo3image', key: 'alt' }, view: 'alt' }); Data Pipeline ============= Complete Load → Edit → Save Flow --------------------------------- :: 1. Load from Database ─────────────────────► HTML String 2. Upcast (HTML → Model) ─────────────────────► Model Element typo3image { fileUid: 123, src: "..." } 3. Edit in Editor ─────────────────────► Model Changes width: "800" → "1200" class: "" → "float-left" 4. Downcast (Model → View) ─────────────────────► View Updates 5. Save to Database ─────────────────────► Data Downcast → HTML String 6. Backend Processing ─────────────────────► RteImagesDbHook processes HTML Magic image processing, URL updates Paste Handling ============== Paste from External Source --------------------------- When pasting HTML from external sources (websites, Word, etc.):: 1. Browser Paste Event ─────────────────────► External HTML 2. Upcast Attempted ─────────────────────► Check: data-htmlarea-file-uid present? ❌ Result: Upcast skipped, treated as regular 3. Fallback Handling ─────────────────────► CKEditor default image handling May need custom paste processor for external images Paste from Same Editor ---------------------- :: 1. Copy typo3image ─────────────────────► Clipboard contains model element 2. Paste ─────────────────────► Direct model copy (no conversion needed) 3. Result ─────────────────────► Duplicate typo3image with same attributes Custom Conversion Patterns =========================== Pattern 1: Transformation During Upcast ---------------------------------------- .. code-block:: javascript editor.conversion.for('upcast').elementToElement({ view: { name: 'img', attributes: ['data-htmlarea-file-uid'] }, model: (viewElement, { writer }) => { // Transform srcset to src const src = viewElement.getAttribute('src') || viewElement.getAttribute('srcset')?.split(',')[0]; // Parse dimensions from style const style = viewElement.getAttribute('style') || ''; const widthMatch = style.match(/width:\s*(\d+)px/); const heightMatch = style.match(/height:\s*(\d+)px/); return writer.createElement('typo3image', { src: src, width: widthMatch ? widthMatch[1] : '', height: heightMatch ? heightMatch[1] : '', // ... other attributes }); } }); Pattern 2: Conditional Downcast -------------------------------- .. code-block:: javascript editor.conversion.for('downcast').elementToElement({ model: 'typo3image', view: (modelElement, { writer }) => { // Different output based on context const width = parseInt(modelElement.getAttribute('width'), 10); // Large images get responsive class if (width > 1200) { attributes['class'] = (attributes['class'] || '') + ' img-responsive'; } return writer.createEmptyElement('img', attributes); } }); Pattern 3: Multi-Element Conversion ------------------------------------ .. code-block:: javascript // Convert linked image to nested structure editor.conversion.for('downcast').elementToStructure({ model: 'typo3image', view: (modelElement, { writer }) => { const linkHref = modelElement.getAttribute('linkHref'); if (linkHref) { // Create nested structure: const img = writer.createEmptyElement('img', imgAttributes); const link = writer.createContainerElement('a', { href: linkHref }); writer.insert(writer.createPositionAt(link, 0), img); return link; } else { // Just image return writer.createEmptyElement('img', imgAttributes); } } }); Debugging Conversions ====================== Logging Upcast -------------- .. code-block:: javascript editor.conversion.for('upcast').elementToElement({ view: { name: 'img', attributes: ['data-htmlarea-file-uid'] }, model: (viewElement, { writer }) => { console.log('Upcasting image:', { src: viewElement.getAttribute('src'), fileUid: viewElement.getAttribute('data-htmlarea-file-uid'), allAttributes: Array.from(viewElement.getAttributes()) }); return writer.createElement('typo3image', { // ... attributes }); } }); Logging Downcast ---------------- .. code-block:: javascript editor.conversion.for('downcast').elementToElement({ model: 'typo3image', view: (modelElement, { writer }) => { console.log('Downcasting typo3image:', { fileUid: modelElement.getAttribute('fileUid'), src: modelElement.getAttribute('src'), allAttributes: Array.from(modelElement.getAttributes()) }); return writer.createEmptyElement('img', attributes); } }); Inspecting Conversion Results ------------------------------ .. code-block:: javascript // After loading content editor.model.change(() => { const root = editor.model.document.getRoot(); for (const item of root.getChildren()) { if (item.name === 'typo3image') { console.log('Found typo3image:', { fileUid: item.getAttribute('fileUid'), src: item.getAttribute('src') }); } } }); Common Issues ============= Issue: Images Not Converting on Load ------------------------------------- **Symptoms**: - HTML loaded but no typo3image elements in model - Images appear as plain text or broken **Causes**: 1. Missing ``data-htmlarea-file-uid`` attribute 2. Upcast converter not registered 3. View matcher too restrictive **Solutions**: Verify HTML has required attributes: .. code-block:: html Check converter registration: .. code-block:: javascript // Verify in browser console console.log(editor.conversion); // Should show upcast/downcast converters Issue: Attributes Lost During Conversion ----------------------------------------- **Symptoms**: - Attributes present in HTML/model - Missing in view/output **Causes**: 1. Attribute not in schema ``allowAttributes`` list 2. Attribute not mapped in conversion 3. Conditional logic skipping attribute **Solutions**: Verify schema allows attribute: .. code-block:: javascript allowAttributes: [ 'src', 'fileUid', /* add missing attribute here */ ] Add to conversion: .. code-block:: javascript // In upcast myCustomAttribute: viewElement.getAttribute('data-custom'), // In downcast 'data-custom': modelElement.getAttribute('myCustomAttribute'), Issue: View Not Updating When Model Changes -------------------------------------------- **Symptoms**: - Model attribute updated - View doesn't reflect change - Need to reload to see changes **Cause**: Missing attribute converter for immediate sync **Solution**: Add attribute converter: .. code-block:: javascript editor.conversion.for('downcast').attributeToAttribute({ model: { name: 'typo3image', key: 'myAttribute' }, view: 'data-my-attribute' }); Performance Optimization ======================== Batch Conversions ----------------- .. code-block:: javascript // Inefficient: Convert one at a time images.forEach(img => { editor.model.change(writer => { writer.setAttribute('class', 'processed', img); }); }); // Efficient: Single model change batch editor.model.change(writer => { images.forEach(img => { writer.setAttribute('class', 'processed', img); }); }); Lazy Attribute Reading ----------------------- .. code-block:: javascript // Inefficient: Read all attributes upfront const allAttrs = { src: viewElement.getAttribute('src'), width: viewElement.getAttribute('width'), height: viewElement.getAttribute('height'), // ... 20 more attributes }; // Efficient: Read only needed attributes const src = viewElement.getAttribute('src'); const fileUid = viewElement.getAttribute('data-htmlarea-file-uid'); Related Documentation ===================== - :ref:`ckeditor-plugin-development-guide` - :ref:`ckeditor-model-element` - :ref:`ckeditor-style-integration` - :ref:`architecture-overview` - :ref:`api-datahandling`