iDevice Content-Storage Patterns¶
This document describes the four ways iDevices store their state inside a content.xml <odeComponent> block. Understanding these patterns is required when writing tools that generate, parse, or round-trip ELPX files programmatically (e.g. AI generators, importers, automated test fixtures).
See also:
- catalog.md — full list of iDevice types and which pattern each one uses
- config-xml.md — per-iDevice config.xml metadata schema
Context: the <odeComponent> wrapper¶
Every iDevice in content.xml is enclosed in an <odeComponent> element. The wrapper carries four identification fields, then the content fields discussed below.
<odeComponent>
<odePageId>20251025070914ABCDEF</odePageId>
<odeBlockId>20251025070914XYZUVW</odeBlockId>
<odeIdeviceId>20251025070914XDRVFP</odeIdeviceId>
<odeIdeviceTypeName>text</odeIdeviceTypeName>
<!-- content fields below -->
</odeComponent>
IDs use the format YYYYMMDDHHmmss + 6 uppercase alphanumeric characters. The odeIdeviceTypeName value is the modern type name from catalog.md.
Pattern 1: Standard JSON¶
When it is used¶
Used by iDevices that have a structured editor backed by a JavaScript component with component-type: json in config.xml. The editor reads jsonProperties on open, modifies it in memory, and writes both jsonProperties and htmlView on save.
Types using this pattern: text, casestudy, image-gallery, magnifier, trueorfalse, form, digcompedu, example, geogebra-activity, scrambled-list, udl-content
Structure¶
<htmlView>— rendered HTML output, wrapped in<![CDATA[ ... ]]>. What the learner sees in the exported package. Regenerated fromjsonPropertiesevery time the iDevice is saved.<jsonProperties>— JSON string, wrapped in<![CDATA[ ... ]]>. The canonical editable state. The editor reads this;htmlViewis derived from it.
Both values are always wrapped in CDATA in v4 — even when the content contains no XML-significant characters — because OdeXmlGenerator.ts:270, 275 emits the wrapper unconditionally. Inside the CDATA section, <, >, ", & are written verbatim (no entity encoding). The single exception is the literal three-character sequence ]]>, which would close the CDATA section prematurely and is therefore split as ]]]]><![CDATA[> by escapeCdata().
How to read it¶
raw_html = odeComponent.htmlView.textContent // already decoded by the XML parser
state_json = json_parse(odeComponent.jsonProperties.textContent)
textContent returns the CDATA payload verbatim — no further decoding is needed.
How to write it¶
- Build the JSON state object.
- Render the HTML from the state object (apply the iDevice's template logic).
- If either string contains the literal sequence
]]>, split it as]]]]><![CDATA[>(theescapeCdatarule). - Wrap each value in
<![CDATA[ ... ]]>and write into<jsonProperties>/<htmlView>.
XML excerpt (type: text)¶
<odeComponent>
<odePageId>20251027202947MKIISA</odePageId>
<odeBlockId>20251027202947EDIAYV</odeBlockId>
<odeIdeviceId>20251025070914XDRVFP</odeIdeviceId>
<odeIdeviceTypeName>text</odeIdeviceTypeName>
<htmlView><div class="exe-text-template"><div class="textIdeviceContent">
<div class="exe-text-activity">
<div><div class="exe-text">
<h1>Manual de eXeLearning 3.0</h1>
<p>Guia practica para crear contenidos educativos.</p>
</div></div>
</div>
</div></div></htmlView>
<jsonProperties>{"ideviceId":"20251025070914XDRVFP",
"textInfoDurationInput":"",
"textInfoParticipantsInput":"",
"textInfoDurationTextInput":"Duracion",
"textInfoParticipantsTextInput":"Agrupamiento",
"textTextarea":"<div class=\"exe-text\">\n<h1>Manual de eXeLearning 3.0</h1>
<p>Guia practica para crear contenidos educativos.</p>\n</div>"
}</jsonProperties>
</odeComponent>
Note that textTextarea inside the JSON itself also HTML-entity-encodes its HTML content (double-encoded relative to the raw file bytes).
Pattern 2: URI-encoded JSON inside htmlView¶
When it is used¶
Used by game-type and complex interactive iDevices. The JSON state is encodeURIComponent(JSON.stringify(data)) and placed inside a hidden <div> whose CSS class follows the naming convention <internal-name>-DataGame js-hidden. The <jsonProperties> element may be absent, empty, or may hold supplementary metadata (duration, grouping, ideviceId) that is not the primary game state.
Types using this pattern:
| type | DataGame class |
|---|---|
az-quiz-game |
rosco-DataGame js-hidden |
beforeafter |
beforeafter-DataGame js-hidden |
challenge |
desafio-DataGame js-hidden |
checklist |
listacotejo-DataGame js-hidden |
classify |
clasifica-DataGame js-hidden |
complete |
completa-DataGame js-hidden |
crossword |
crucigrama-DataGame js-hidden |
discover |
descubre-DataGame js-hidden |
dragdrop |
dragdrop-DataGame js-hidden |
flipcards |
flipcards-DataGame js-hidden |
guess |
adivina-DataGame js-hidden |
hidden-image |
hiddenimage-DataGame js-hidden |
identify |
identifica-DataGame js-hidden |
map |
mapa-DataGame js-hidden |
mathematicaloperations |
mathoperations-DataGame js-hidden |
mathproblems |
mathproblems-DataGame js-hidden |
padlock |
candado-DataGame js-hidden |
periodic-table |
periodic-table-DataGame js-hidden |
progress-report |
informe-DataGame js-hidden |
puzzle |
puzzle-DataGame js-hidden |
quick-questions |
quext-DataGame js-hidden |
quick-questions-multiple-choice |
selecciona-DataGame js-hidden |
quick-questions-video |
vquext-DataGame js-hidden |
relate |
relaciona-DataGame js-hidden |
select-media-files |
seleccionamedias-DataGame js-hidden |
sort |
ordena-DataGame js-hidden |
trivial |
trivial-DataGame js-hidden |
word-search |
sopa-DataGame js-hidden |
Structure¶
Inside <htmlView>, the rendered HTML includes a hidden <div> that holds the URI-encoded game data:
<div class="<name>-DataGame js-hidden">%7B%22typeGame%22%3A%22...</div>
The value is encodeURIComponent(JSON.stringify(gameData)). The %-encoded payload sits inside the CDATA section of <htmlView>, so the percent-encoding flows through the XML serialisation untouched (CDATA does not reinterpret %, <, or any other character).
How to read it¶
raw_html = odeComponent.htmlView.textContent // CDATA body — verbatim
dom = parse_html(raw_html)
div = dom.querySelector('[class*="DataGame"][class*="js-hidden"]')
game_data = json_parse(decodeURIComponent(div.textContent.trim()))
How to write it¶
- Build the game data JSON object.
encoded = encodeURIComponent(JSON.stringify(gameData))- Inject the string as the text content of the
<div class="<name>-DataGame js-hidden">element inside the full rendered HTML. - HTML-entity-encode the full HTML and write it into
<xmlView>. - If the iDevice also uses
<jsonProperties>for supplementary metadata (ideviceId, duration, grouping), write that separately using Pattern 1 encoding for those fields.
XML excerpt (type: flipcards)¶
<odeComponent>
<odePageId>20251021091936PQRSTU</odePageId>
<odeBlockId>20251021091936ABCXYZ</odeBlockId>
<odeIdeviceId>20251021091936VCTATJ</odeIdeviceId>
<odeIdeviceTypeName>flipcards</odeIdeviceTypeName>
<htmlView><div class="flipcards-IDevice">
<div class="flipcards-instructions gameQP-instructions">
<p>Haz clic sobre cada carta para descubrir el nombre de su animal</p>
</div>
<div class="flipcards-DataGame js-hidden">%7B%22typeGame%22%3A%22FlipCards%22%2C
%22author%22%3A%22%22%2C%22randomCards%22%3Atrue%2C%22instructions%22%3A%22...%22%2C
%22cardsGame%22%3A%5B%7B%22url%22%3A%22...%22%7D%5D%7D
</div>
</div></htmlView>
<jsonProperties>{"ideviceId":"20251021091936VCTATJ",
"textInfoDurationInput":"",
"textInfoParticipantsInput":""}</jsonProperties>
</odeComponent>
Decoded, the DataGame div text is:
{
"typeGame": "FlipCards",
"author": "",
"randomCards": true,
"instructions": "<p>Haz clic sobre cada carta...</p>",
"cardsGame": [{ "url": "{{context_path}}/..." }]
}
Note on crossword and some other games¶
Some games (crossword, hidden-image, puzzle, beforeafter, dragdrop, periodic-table) have <jsonProperties> absent entirely (not just empty). The DataGame div in <htmlView> is the only source of truth for those types.
Pattern 3: Embedded JSON block in htmlView¶
When it is used¶
Used by interactive-video (and the related quick-questions-video in some export versions). The JSON data for interactive slides is embedded directly inside <htmlView> as the content of a specially identified element, rather than URI-encoded in a DataGame div.
Types using this pattern: interactive-video
Structure¶
There are two sub-variants depending on eXeLearning version:
Modern (v3.x exports): a <script> tag with type="application/json":
<script id="exe-interactive-video-contents" type="application/json">
{"slides": [...], "i18n": {...}}
</script>
Older exports: a <div> with style="display:none":
<div id="exe-interactive-video-contents" style="display: none">
{"slides": [...], "i18n": {...}}
</div>
Both are inside the CDATA-wrapped <htmlView>. The <jsonProperties> element is also present (also CDATA-wrapped) and holds a separately maintained editor copy of the video configuration; the two may diverge if the user edits one path and not the other.
How to read it¶
raw_html = odeComponent.htmlView.textContent // CDATA body — verbatim
dom = parse_html(raw_html)
el = dom.getElementById('exe-interactive-video-contents')
slides = json_parse(el.textContent.trim())
How to write it¶
- Build the slides JSON object.
- Serialize to a JSON string (no URI encoding needed).
- Place the JSON string as the text content of
<script id="exe-interactive-video-contents" type="application/json">inside the full HTML structure. - HTML-entity-encode the full HTML and write into
<htmlView>. - Write matching state into
<jsonProperties>using Pattern 1 encoding.
XML excerpt (type: interactive-video)¶
<odeComponent>
<odeIdeviceId>20251027120355IWQHYP</odeIdeviceId>
<odeIdeviceTypeName>interactive-video</odeIdeviceTypeName>
<htmlView><div class="exe-interactive-video-content-before">
<p>Video sobre el suelo</p>
</div>
<div class="game-evaluation-ids js-hidden"
data-id="20251027120355IWQHYP"
data-evaluationb="false" data-evaluationid=""></div>
<div class="exe-interactive-video">
<p id="exe-interactive-video-file" class="js-hidden">
<a href="https://www.youtube.com/watch?v=ALO_ukssQLg">
com/watch?v=ALO_ukssQLg</a>
</p>
<script id="exe-interactive-video-contents"
type="application/json">
{"slides":[
{"type":"text","text":"<p>Por que es tan importante el suelo?</p>",
"startTime":45,"current":false},
{"type":"image","url":1,"description":"Formacion del suelo",
"startTime":118}
],"i18n":{"start":"Empezar","results":"Resultado"}}
</script>
</div></htmlView>
<jsonProperties>{"ideviceId":"20251027120355IWQHYP",
"textInfoDurationInput":"","textInfoParticipantsInput":"",
"textTextarea":"<div class=\"exe-interactive-video ...\">..."
}</jsonProperties>
</odeComponent>
Pattern 4: htmlView-only¶
When it is used¶
Used by iDevices where the entire editable content is rendered HTML with no separate machine-readable JSON state. The editor writes HTML directly; on re-edit it parses the HTML back.
Types using this pattern: rubric, external-website, download-source-file
Note: udl-content has <jsonProperties> present (Pattern 1) but the HTML in htmlView contains multiple <section> blocks representing the UDL principles. The JSON state stores the same HTML sections. This is still Pattern 1, not Pattern 4.
Structure¶
<htmlView>— the complete rendered HTML, wrapped in<![CDATA[ ... ]]>like every otherhtmlView.<jsonProperties>— absent or empty.
How to read it¶
raw_html = odeComponent.htmlView.textContent // CDATA body — verbatim
There is no secondary JSON to decode. Any structured data (iframe URL for external-website, table content for rubric) must be parsed from the HTML directly.
How to write it¶
- Build the HTML string directly.
- HTML-entity-encode it.
- Write into
<htmlView>. Omit or leave empty the<jsonProperties>element.
XML excerpt (type: external-website)¶
<odeComponent>
<odeIdeviceId>20251021091936EXTURL</odeIdeviceId>
<odeIdeviceTypeName>external-website</odeIdeviceTypeName>
<htmlView><div id="iframeWebsiteIdevice">
<iframe src="https://cedec.intef.es/"
width="600" height="300"
style="width:100%;">
</iframe>
<div class="iframe-error-message" style="display:none;">
No se puede mostrar un iframe en HTTP en una web HTTPS.
</div>
</div></htmlView>
</odeComponent>
XML excerpt (type: rubric)¶
<odeComponent>
<odeIdeviceId>20251021091936RUBRIC</odeIdeviceId>
<odeIdeviceTypeName>rubric</odeIdeviceTypeName>
<htmlView><table class='exe-table'>
<caption>Rubrica para evaluar un trabajo escrito</caption>
<thead><tr>
<th>&nbsp;</th>
<th>4 Excelente</th>
<th>3 Satisfactorio</th>
<th>2 Mejorable</th>
<th>1 Insuficiente</th>
</tr></thead>
<tbody>
<tr>
<th>Aspectos formales</th>
<td>Se presenta en plazo ...</td>
...
</tr>
</tbody>
</table></htmlView>
</odeComponent>
Edge cases and caveats¶
scrambled-list: Pattern 1 with game-flavoured JSON¶
scrambled-list is classified as Pattern 1 (standard JSON in jsonProperties). However its JSON payload uses a typeGame field ("typeGame": "ScrambledList") similar to DataGame payloads. This is not URI-encoded; the JSON is written directly into <jsonProperties>. The htmlView contains the rendered sortable list HTML but no *-DataGame js-hidden div.
geogebra-activity: Pattern 1, but htmlView has DataGame-style evaluation div¶
geogebra-activity uses Pattern 1 (JSON in jsonProperties). The htmlView may contain a game-evaluation-ids js-hidden div for SCORM scoring, but the main configuration state is in jsonProperties.
quick-questions-video: DataGame in htmlView¶
Despite the name similarity to interactive-video, quick-questions-video uses Pattern 2 (DataGame, vquext-DataGame js-hidden), not Pattern 3.
Asset path placeholder {{context_path}}¶
Image and media URLs inside both htmlView and jsonProperties use {{context_path}}/ as a prefix, followed by the iDevice ID and filename. At export time the exporter replaces {{context_path}} with the relative path to the assets directory.
Decision cheat-sheet: which pattern for a new iDevice?¶
Does the iDevice have a structured JSON editor
with component-type=json in config.xml?
YES --> Does it use a game runtime that needs
URI-encoded state in the DOM?
YES --> Pattern 2 (DataGame)
NO --> Is the state a video slide deck?
YES --> Pattern 3 (embedded script/div)
NO --> Pattern 1 (standard JSON)
NO --> Pattern 4 (htmlView-only)
Quick rules: - Simple content iDevices (text, case study, gallery, magnifier): Pattern 1. - Game/activity iDevices with a game engine (flipcards, crossword, quiz, sort, etc.): Pattern 2. - Interactive video only: Pattern 3. - Legacy presentational iDevices with no JSON model (rubric, iframe embed, file download): Pattern 4.