.. _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
**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
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`