Repository navigation
Expand file tree
/
Copy pathllms-full.txt
More file actions
9101 lines (7074 loc) · 406 KB
/
Copy pathllms-full.txt
File metadata and controls
9101 lines (7074 loc) · 406 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# eXeLearning — Full Documentation Bundle
> This is a single-file concatenation of the eXeLearning ELPX-format
> documentation set, plus the project's architecture and conventions
> documents. It is generated by `scripts/build-llms-full.sh` from the
> Markdown sources under `doc/`. Use this file when a context window
> prefers one large document over following links from `/llms.txt`.
> Source files (in order):
> - `doc/elpx-format.md`
> - `doc/elpx-format/container.md`
> - `doc/elpx-format/content-xml.md`
> - `doc/elpx-format/ids.md`
> - `doc/elpx-format/metadata.md`
> - `doc/elpx-format/pages-blocks.md`
> - `doc/elpx-format/idevices/catalog.md`
> - `doc/elpx-format/idevices/patterns.md`
> - `doc/elpx-format/idevices/config-xml.md`
> - `doc/elpx-format/idevices/snippets.md`
> - `doc/elpx-format/themes.md`
> - `doc/elpx-format/libraries.md`
> - `doc/elpx-format/assets.md`
> - `doc/elpx-format/screenshot.md`
> - `doc/elpx-format/export-pipeline.md`
> - `doc/elpx-format/import-pipeline.md`
> - `doc/elpx-format/validation.md`
> - `doc/elpx-format/ai-generation.md`
> - `doc/elpx-format/examples/minimal-content-xml.md`
> - `doc/elpx-format/examples/multi-page-content-xml.md`
> - `doc/elpx-format/examples/full-package-tree.md`
> - `doc/architecture.md`
> - `doc/conventions.md`
---
# ===== doc/elpx-format.md =====
# ELPX Format Documentation
This is the hub for the `.elpx` file format used by eXeLearning v3+. The format is documented across a focused subdoc set under [`doc/elpx-format/`](elpx-format/) — start here, then jump to the specific reference you need.
> **For AI / LLM agents**: read [`elpx-format/ai-generation.md`](elpx-format/ai-generation.md) first, then the [`/llms.txt`](../llms.txt) index at the repo root, then the subdocs cited from each.
> **For the legacy `.elp` format** (eXeLearning 2.x), see [contentv3-format.md](contentv3-format.md).
---
## What is `.elpx`?
An `.elpx` file is a **ZIP archive** containing a complete, self-contained eXeLearning project. It was introduced in eXeLearning v3.0 to replace the legacy Python-pickle `.elp` format. A single `.elpx` carries:
- **`content.xml`** — the project structure in ODE 2.0 XML, validatable against a bundled DTD, re-importable for editing.
- **`index.html` + `html/`** — pre-rendered HTML output, viewable directly in a browser without unpacking.
- **`theme/`, `libs/`, `idevices/`, `content/`** — every CSS, JavaScript, image, font, and asset the rendered content depends on.
- **`screenshot.png`** — 1280×720 PNG project thumbnail at the ZIP root (always present in v4 packages; legacy files without one can be patched with [`scripts/add-screenshot.ts`](../scripts/add-screenshot.ts)).
Because the package contains both the editable source and the rendered output, `.elpx` is the recommended exchange format: human-readable in any browser, re-importable into eXeLearning for editing, and validatable end-to-end.
---
## Quick reference
| Property | Value |
|----------|-------|
| Container | ZIP (deflate) |
| Required root files | `content.xml`, `content.dtd`, `index.html`, `screenshot.png` |
| Required directories | `theme/`, `libs/`, `idevices/<type>/` for each used type |
| Optional root files | `search_index.js` (when search box is enabled) |
| Asset layout | `content/resources/<folderPath>/<filename>` (user folders preserved; legacy v3 UUID subfolders normalised away) |
| Asset reference form | `{{context_path}}/<exportPath>` inside `<htmlView>` / `<jsonProperties>` where `<exportPath>` is `<folderPath>/<filename>` (or just `<filename>` when no folder); `{{context_path}}` resolves to `content/resources/` at render time |
| XML namespace | `http://www.intef.es/xsd/ode` |
| Format version | ODE `2.0` (root `<ode version="2.0">`) |
| Runtime version | constant `ODE_VERSION` = `"3.0"` (XML key `exe_version`) |
| ID format | `[0-9]{14}[A-Z0-9]{6}` |
| iDevice catalog | 43 types (see [catalog](elpx-format/idevices/catalog.md)) |
| Schemas | DTD bundled at root + XSD in repo |
---
## Documentation map
### Container and structure
- **[container.md](elpx-format/container.md)** — every file and directory inside an `.elpx` ZIP, mandatory vs optional, where each entry comes from in the export pipeline.
- **[content-xml.md](elpx-format/content-xml.md)** — full element-by-element reference for `content.xml` (DOCTYPE, root, `<userPreferences>`, `<odeResources>`, `<odeProperties>`, `<odeNavStructures>`, plus the bundled DTD verbatim and pointer to the XSD).
- **[ids.md](elpx-format/ids.md)** — ODE identifier format, generation, lifecycle, cross-hierarchy synchronization rules.
- **[metadata.md](elpx-format/metadata.md)** — every `pp_*` property key, type, default, plus `userPreferences` and `odeResources`.
- **[pages-blocks.md](elpx-format/pages-blocks.md)** — flat-list navigation model, parent-child IDs, ordering, `exe-node:` internal links.
### iDevices
- **[idevices/catalog.md](elpx-format/idevices/catalog.md)** — every iDevice type (modern names, legacy/FPD aliases, downloadable flag, category).
- **[idevices/patterns.md](elpx-format/idevices/patterns.md)** — the four content-storage patterns (Standard JSON, URI-encoded JSON, `<script type="application/json">`, `htmlView`-only) with decision flow.
- **[idevices/config-xml.md](elpx-format/idevices/config-xml.md)** — the per-iDevice `config.xml` schema in `public/files/perm/idevices/base/<type>/`.
- **[idevices/snippets.md](elpx-format/idevices/snippets.md)** — copy-pasteable `<odeComponent>` XML for every type, extracted from real fixtures.
### Resources, themes, assets
- **[themes.md](elpx-format/themes.md)** — theme bundle layout, the 6 in-tree themes (`base`, `flux`, `neo`, `nova`, `universal`, `zen`), per-theme `config.xml`, custom user themes.
- **[libraries.md](elpx-format/libraries.md)** — `libs/` contents and inclusion rules (always-bundled vs conditional, including `common_i18n.js` generation and `elpx-manifest.js`).
- **[assets.md](elpx-format/assets.md)** — the asset URL lifecycle (`asset://` → `{{context_path}}/content/resources/` → relative path).
- **[screenshot.md](elpx-format/screenshot.md)** — root `screenshot.png` spec, dimensions, generation hook, base64 round-trip.
### Pipelines
- **[export-pipeline.md](elpx-format/export-pipeline.md)** — end-to-end export flow with file:line references to `Html5Exporter` / `ElpxExporter` / `OdeXmlGenerator`.
- **[import-pipeline.md](elpx-format/import-pipeline.md)** — three-phase import flow, modern vs legacy fallback, ID remapping.
### Validation and AI generation
- **[validation.md](elpx-format/validation.md)** — DTD vs XSD coverage, mandatory presence checklist, common rejection causes, how to run `xmllint` and `OdeXmlValidator`.
- **[ai-generation.md](elpx-format/ai-generation.md)** — rules and recipes for LLMs producing `.elpx`: the ten non-negotiables, recommended pipeline, pre-flight checklist, prompt-engineering hints.
### Examples
- **[examples/minimal-content-xml.md](elpx-format/examples/minimal-content-xml.md)** — smallest valid `content.xml` (1 page, 1 block, 1 text iDevice) plus a `zip` recipe.
- **[examples/multi-page-content-xml.md](elpx-format/examples/multi-page-content-xml.md)** — hierarchy + multiple iDevices covering all four storage patterns.
- **[examples/full-package-tree.md](elpx-format/examples/full-package-tree.md)** — annotated `unzip -l` of `test/fixtures/really-simple-test-project.elpx`.
---
## ZIP layout at a glance
```
project.elpx (ZIP)
├── content.xml # ODE 2.0 project structure (re-importable)
├── content.dtd # DTD for XML validation (bundled copy)
├── index.html # Entry point (first page rendered as HTML)
├── screenshot.png # 1280×720 PNG project thumbnail (always present in v4)
├── html/
│ ├── page-title.html # Additional pages (one file per page)
│ └── ...
├── content/
│ ├── css/
│ │ ├── base.css # Base stylesheet
│ │ └── icons/*.svg # ~75 SVG icons used by the eXeLearning runtime UI
│ ├── img/
│ │ └── exe_powered_logo.png
│ └── resources/ # User-uploaded assets (no UUID subfolders in v4)
│ ├── photo.jpg # asset with no folderPath
│ ├── audio-clip.mp3
│ ├── document.pdf
│ └── photos/ # user-created folder, preserved verbatim
│ └── vacation/sunset.jpg
├── libs/
│ ├── jquery/jquery.min.js
│ ├── bootstrap/bootstrap.bundle.min.js
│ ├── bootstrap/bootstrap.min.css
│ ├── common.js
│ ├── common_i18n.js # Generated per export language
│ ├── exe_export.js
│ ├── favicon.ico
│ ├── exe_atools/ # Accessibility toolbar (conditional)
│ └── ... # Other conditionally-bundled libraries
├── theme/
│ ├── config.xml
│ ├── style.css
│ ├── style.js
│ ├── screenshot.png # THEME's preview thumbnail (distinct from the project's)
│ ├── icons/
│ └── img/
├── idevices/
│ ├── text/
│ │ ├── text.js
│ │ ├── text.css
│ │ └── text.html
│ └── <type>/ # One folder per iDevice type used in the project
└── search_index.js # Optional: present when search box is enabled
```
Detailed mandatory-vs-optional matrix: [container.md](elpx-format/container.md).
---
## Differences from legacy `.elp`
| Aspect | `.elpx` (modern) | `.elp` (legacy) |
|--------|------------------|-----------------|
| Content file | `content.xml` (ODE 2.0) | `contentv3.xml` (Python pickle XML) |
| Root element | `<ode xmlns="...">` | `<instance class="exe.engine.package.Package">` |
| Serialization | Native XML | Python object serialization |
| Hierarchy | Flat list with `odeParentPageId` | Nested `Node` instances |
| Content storage | `<htmlView>` + `<jsonProperties>` (CDATA-wrapped) | `<unicode content="true">` |
| Metadata | `<odeProperty>` elements | Dictionary key-value pairs |
| IDs | `YYYYMMDDHHmmss` + 6 chars | Sequential integers |
| DTD validation | Supported (bundled `content.dtd`) | Not feasible (dynamic structure) |
| Asset paths | `{{context_path}}/content/resources/` | `resources/` |
| HTML output | Included in ZIP | Not included |
| Screenshot | Optional `screenshot.png` at root | None |
For full legacy details, see [contentv3-format.md](contentv3-format.md).
---
## Implementation files (ground-truth code)
| File | Role |
|------|------|
| `src/shared/export/exporters/ElpxExporter.ts` | Builds the ZIP archive (extends `Html5Exporter`) |
| `src/shared/export/exporters/Html5Exporter.ts` | Page generation, theme/library/asset bundling |
| `src/shared/export/exporters/BaseExporter.ts` | Abstract base with `addFilenamesToAssetUrls()` |
| `src/shared/export/generators/OdeXmlGenerator.ts` | Generates `content.xml` from `ExportMetadata` + pages |
| `src/shared/export/metadata-properties.ts` | Single source of truth for `pp_*` property mapping |
| `src/shared/export/constants.ts` | DTD literal, BASE_LIBRARIES, LICENSE_REGISTRY, IDEVICE_TYPE_MAP, LIBRARY_PATTERNS |
| `src/shared/import/ElpxImporter.ts` | Parses `.elpx` and `.elp` files into a Yjs document |
| `src/shared/import/LegacyXmlParser.ts` | Legacy CamelCase type-name remapping for `.elp` files |
| `src/services/xml/xml-parser.ts` | ODE XML parsing helpers |
| `src/services/xml/ode-xml-validator.ts` | DTD/XSD validator |
| `public/app/schemas/ode/content.dtd` | Canonical DTD reference |
| `public/app/schemas/ode/ode-content.xsd` | Stricter XSD with regex-validated IDs and enumerated types |
---
## Test fixtures (real `.elpx` files in this repo)
| Fixture | Use |
|---------|-----|
| `test/fixtures/really-simple-test-project.elpx` | Minimal multi-page project — annotated tree at [examples/full-package-tree.md](elpx-format/examples/full-package-tree.md) |
| `test/fixtures/todos-los-idevices_dos_informes.elpx` | Every iDevice type — primary source for [idevices/snippets.md](elpx-format/idevices/snippets.md) |
| `test/fixtures/Manual de eXeLearning 3.0.elpx` | Full real-world manual covering `text`, `casestudy`, `image-gallery`, `magnifier`, `external-website`, `download-source-file`, `udl-content`, `rubric` |
| `test/fixtures/basic-example-with-custom-theme.elpx` | Custom theme bundling |
| `test/fixtures/arrows.elpx` | Diagram / vector content |
To inspect a fixture: `unzip -l "test/fixtures/<name>.elpx"`. To extract: `unzip -d /tmp/elpx-inspect "test/fixtures/<name>.elpx"`.
---
## See also
- [`/llms.txt`](../llms.txt) — top-level index for LLM consumption (per [llmstxt.org](https://llmstxt.org/))
- [`/llms-full.txt`](../llms-full.txt) — full doc bundle in a single file for LLMs that cannot follow links
- [doc/architecture.md](architecture.md) — system architecture overview
- [doc/conventions.md](conventions.md) — code conventions
- [doc/development/styles.md](development/styles.md) — theme authoring guide
- [doc/development/real-time.md](development/real-time.md) — Yjs and WebSocket model
- [doc/development/rest-api.md](development/rest-api.md) — REST API v1 (external integrations)
- [doc/contentv3-format.md](contentv3-format.md) — legacy `.elp` format
# ===== doc/elpx-format/container.md =====
# ELPX Container: ZIP Structure
An `.elpx` file is a standard ZIP archive produced by `ElpxExporter.ts`. It bundles a complete HTML5 rendition of the project together with a re-importable `content.xml`. This document describes every entry in the archive, where each one comes from, and which entries are required by the importer.
See also: [ELPX format overview](../elpx-format.md) | [IDs](ids.md) | [Pages and blocks](pages-blocks.md) | [Metadata](metadata.md)
---
## Folder tree (fix-simple fixture)
The tree below is extracted from the unzipped fixture at `/tmp/elpx-docs-work/fix-simple/`. Individual icon SVG files in `content/css/icons/` are collapsed for brevity.
```
project.elpx (ZIP)
├── content.xml # ODE 2.0 project structure (re-importable)
├── content.dtd # DTD for content.xml validation (always bundled in v4)
├── index.html # First page rendered as HTML
├── screenshot.png # 1280×720 PNG project thumbnail (always present in v4)
├── search_index.js # (optional) Search index — only when pp_addSearchBox=true
├── content/
│ ├── css/
│ │ ├── base.css # Base stylesheet
│ │ └── icons/ # … 75 icon SVGs (exe-*.svg, plus.svg, etc.)
│ ├── img/
│ │ └── exe_powered_logo.png
│ └── resources/ # Project assets — no UUID subfolders in v4
│ ├── photo.jpg # asset with no folderPath
│ ├── audio-clip.mp3
│ ├── document.pdf
│ └── photos/ # user-created folder, preserved verbatim
│ └── vacation/sunset.jpg # nested user folder is fine
├── html/
│ ├── page-1-1.html
│ ├── page-1-1-1.html
│ ├── page-1-2.html
│ ├── page-2.html
│ └── page-2-1.html
├── idevices/
│ └── text/
│ ├── text.css
│ ├── text.html
│ └── text.js
├── libs/
│ ├── bootstrap/
│ │ ├── bootstrap.bundle.min.js
│ │ ├── bootstrap.bundle.min.js.map
│ │ ├── bootstrap.min.css
│ │ └── bootstrap.min.css.map
│ ├── common.js
│ ├── common_i18n.js
│ ├── exe_atools/ # Accessibility toolbar assets
│ ├── exe_export.js
│ ├── favicon.ico
│ └── jquery/
│ └── jquery.min.js
├── theme/
│ ├── config.xml
│ ├── icons/ # Theme-specific block icons (PNG)
│ ├── img/
│ │ ├── icons.png
│ │ └── licenses.gif
│ ├── screenshot.png # Theme preview (not the project screenshot)
│ ├── style.css
│ └── style.js
└── custom/ # (optional) Custom CSS/JS injected by pp_customStyles
```
---
## Mandatory vs. optional entry matrix
| Entry | Required in ZIP | Required by importer | Notes |
|---|---|---|---|
| `content.xml` | Yes | Yes — mandatory | Missing = import error |
| `content.dtd` | Yes — always bundled in v4 | No | For offline XML validation; emitted by `ElpxExporter` from `ODE_DTD_CONTENT` (`constants.ts:1006`) |
| `index.html` | Yes | No | Pre-rendered first page; ignored on import |
| `screenshot.png` (archive root) | Yes in v4 | No | 1280×720 PNG project thumbnail. Required for v4 packages so external systems (LMS, file managers, repositories) can show a preview. Stored in Yjs `metadata.screenshot` on import. See [screenshot.md](screenshot.md). |
| `html/*.html` | Yes (one per extra page) | No | Pre-rendered pages; ignored on import |
| `content/css/base.css` | Yes | No | Rendering only |
| `content/css/icons/` | Yes | No | Rendering only |
| `content/img/exe_powered_logo.png` | Conditional | No | Added only when `pp_addExeLink` is `true` |
| `content/resources/<filename>` | When project has assets | No | Flat layout. Asset data; importer reads these for `asset://` mapping. See [assets.md](assets.md). |
| `libs/` | Yes | No | jQuery, Bootstrap, i18n scripts |
| `theme/` | Yes | No | Theme CSS/JS; re-applied on export after import |
| `idevices/<type>/` | One per iDevice type used | No | iDevice-specific CSS/JS |
| `custom/` | Optional | No | Written only when `pp_customStyles` is non-empty |
| `search_index.js` | Optional | No | Written only when `pp_addSearchBox` is `true` |
Notes:
- `content/resources/` entries are read by the importer during the asset extraction phase (`importAssets()`), so they are effectively required when the project references any assets. Without them, asset references in `content.xml` will resolve to missing files.
- `content/resources/` mirrors the project's asset folder tree in v4: assets without a `folderPath` live at the root of `content/resources/`, and any user-created folders (e.g. `photos/`, `lesson-1/handouts/`) appear as real subdirectories under the same path. The legacy v3 per-asset UUID subfolder pattern (`content/resources/<14-digit-timestamp><6-char-suffix>/<filename>`) is **not** part of v4 and is normalised by [`scripts/flatten-elpx.ts`](../../scripts/flatten-elpx.ts) (which only collapses folders matching that exact regex; user folders are preserved untouched).
- `screenshot.png` and `content.dtd` are produced for every export by `ElpxExporter`. If you receive a v3-era `.elpx` without one or both, run `scripts/add-screenshot.ts` and re-export through `elp:convert` to bring it up to the v4 baseline.
---
## Where each entry comes from
All ZIP assembly happens in `ElpxExporter.ts`. `ElpxExporter` extends `Html5Exporter`; the HTML rendering pass runs first, then ELPX-specific files are added in a second section.
| Entry | Producer method / location |
|---|---|
| `index.html` | `Html5Exporter` — page render loop, first page is always `index.html` |
| `html/<slug>.html` | `Html5Exporter` — one call per additional page, slug derived from page title |
| `content/css/base.css` | `Html5Exporter` — `addFile('content/css/base.css', baseCss)` |
| `content/css/icons/` | `Html5Exporter` — iterates icon directory from theme assets |
| `content/img/exe_powered_logo.png` | `Html5Exporter` — conditional on `pp_addExeLink` |
| `content/resources/` | `Html5Exporter` — iterates project assets from Yjs `assets` Y.Map |
| `libs/` | `Html5Exporter` — iterates `BASE_LIBRARIES` and detected `LIBRARY_PATTERNS` |
| `theme/` | `Html5Exporter` — iterates active theme files |
| `idevices/<type>/` | `Html5Exporter` — one subdirectory per iDevice type present in the project |
| `search_index.js` | `Html5Exporter` — conditional on `pp_addSearchBox` (`addFile('search_index.js', ...)`) |
| `content.xml` | `ElpxExporter` — `generateOdeXml()` via `OdeXmlGenerator.ts`, then `this.zip.addFile('content.xml', contentXml)` |
| `content.dtd` | `ElpxExporter` — `this.zip.addFile(ODE_DTD_FILENAME, ODE_DTD_CONTENT)` where `ODE_DTD_FILENAME = 'content.dtd'` (`constants.ts:995`) |
| `screenshot.png` (root) | `ElpxExporter` — `this.zip.addFile('screenshot.png', screenshotBuffer)`. v4 always emits one: either the user-set screenshot or a generated thumbnail. |
| `custom/` | `Html5Exporter` — written when `pp_customStyles` is non-empty |
The final ZIP is assembled using JSZip (browser) or a compatible in-process archiver (server). The `ElpxExporter` accumulates all entries via `this.zip.addFile(path, content)` calls and serializes the archive in one step at the end of `export()`.
---
## Re-importable requirement
The importer (`ElpxImporter.ts`) only **strictly** requires:
1. `content.xml` — mandatory. Its absence throws `'content.xml is missing'`.
2. `content/resources/<filename>` — required per asset referenced in `content.xml`. Assets are mapped from `{{context_path}}/<filename>` (or the legacy `{{context_path}}/content/resources/<filename>` form) back to internal `asset://` URIs during the asset extraction phase.
3. `screenshot.png` at the archive root — read when present and stored as a base64 data URL under the `screenshot` metadata key.
In other words a `.elpx` will round-trip into the editor as long as it has `content.xml` plus any referenced assets. HTML files, libraries, theme files, and iDevice assets are not read by the importer and can be absent without error.
That said, for **publication and exchange** the v4 baseline is stricter: every released `.elpx` should carry `content.dtd`, `screenshot.png`, `index.html`, the `theme/`, `libs/`, and `idevices/<type>/` directories so the package is offline-viewable in any browser without a re-import. The exporter always produces this complete set; only round-tripped imports of older v3 fixtures may need to be patched up via [`scripts/add-screenshot.ts`](../../scripts/add-screenshot.ts).
For EPUB3 archives, the importer also handles `EPUB/content.xml` by stripping the `EPUB/` prefix before processing, making the path equivalent to a root-level `content.xml`.
---
## Minimum content for a v4 `.elpx`
The smallest **v4-compliant** package — one that passes validation, opens cleanly in any browser, and re-imports without warnings — must contain at minimum:
```
project.elpx
├── content.xml # ODE 2.0 XML, references 'content.dtd' in DOCTYPE
├── content.dtd # bundled DTD (constant ODE_DTD_CONTENT)
├── index.html # rendered first page
├── screenshot.png # 1280×720 project thumbnail (PNG)
├── theme/ # at minimum config.xml + style.css + style.js
├── libs/ # base libraries (jQuery, Bootstrap, common.js, exe_export.js, common_i18n.js)
└── idevices/<type>/ # CSS/JS for every iDevice type used
```
If `content.xml` references assets, add them under `content/resources/<filename>` (flat). If pages other than `index.html` exist, add `html/<slug>.html` per page.
A package that lacks `screenshot.png` or `content.dtd` will still re-import (the importer is lenient), but it is considered out-of-spec for v4. See [validation.md](validation.md) for the full checklist and [ai-generation.md](ai-generation.md) for an LLM-friendly version.
---
## Compression
`.elpx` is a standard ZIP archive. Compression is handled by JSZip in the browser exporter. The `ElpxExporter` does not set a specific compression level per file; JSZip defaults apply. Binary assets (images, fonts) are stored without re-compression. Text files (`content.xml`, HTML, CSS, JS) benefit from DEFLATE compression.
The archive is finalized with `this.zip.generateAsync({ type: 'uint8array' })` (browser) or the equivalent server-side call in `ElpxExporter.export()`.
# ===== doc/elpx-format/content-xml.md =====
# `content.xml` Reference
Authoritative reference for the ODE 2.0 XML document that lives at the root of every `.elpx` archive. This is the file an importer reads to reconstruct a project.
> **Hub**: [doc/elpx-format.md](../elpx-format.md). Sibling subdocs:
> [container.md](container.md), [ids.md](ids.md), [metadata.md](metadata.md),
> [pages-blocks.md](pages-blocks.md), [idevices/catalog.md](idevices/catalog.md),
> [idevices/patterns.md](idevices/patterns.md), [validation.md](validation.md).
---
## At a glance
| Property | Value |
|----------|-------|
| Filename | `content.xml` (always at ZIP root) |
| Encoding | UTF-8, XML 1.0 |
| Namespace | `http://www.intef.es/xsd/ode` |
| Format version | ODE `2.0` (root attribute `version="2.0"`) |
| Schema | DTD `content.dtd` bundled at ZIP root + XSD `ode-content.xsd` (in repo, not bundled) |
| Top-level elements (in order) | `userPreferences?`, `odeResources?`, `odeProperties?`, `odeNavStructures` |
| Generator | [`src/shared/export/generators/OdeXmlGenerator.ts`](../../src/shared/export/generators/OdeXmlGenerator.ts) |
| Importer | [`src/shared/import/ElpxImporter.ts`](../../src/shared/import/ElpxImporter.ts) + [`src/services/xml/xml-parser.ts`](../../src/services/xml/xml-parser.ts) |
---
## Document declaration
Every `content.xml` produced by [`OdeXmlGenerator.generateOdeXml()`](../../src/shared/export/generators/OdeXmlGenerator.ts) starts with:
```xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE ode SYSTEM "content.dtd">
<ode xmlns="http://www.intef.es/xsd/ode" version="2.0">
...
</ode>
```
Notes:
- The DOCTYPE filename is the constant `ODE_DTD_FILENAME` (= `"content.dtd"`) defined in [`src/shared/export/constants.ts:995`](../../src/shared/export/constants.ts).
- The DOCTYPE line is omitted only when the same generator is reused for SCORM/IMS exports (option `includeDoctype: false`). For `.elpx` it is always emitted.
- The XML declaration encoding is fixed to `UTF-8`. All text content (page titles, iDevice content, metadata) is UTF-8.
- `xmlns` is fixed by the DTD (`<!ATTLIST ode xmlns CDATA #FIXED "http://www.intef.es/xsd/ode">`); a parser that does not see this exact namespace MUST reject the file.
- `version` is currently `"2.0"`. The string `"3.0"` is the eXeLearning runtime version and lives separately inside `<odeResources>` under the key `exe_version`.
---
## Root `<ode>` element
```dtd
<!ELEMENT ode (userPreferences?, odeResources?, odeProperties?, odeNavStructures)>
<!ATTLIST ode
xmlns CDATA #FIXED "http://www.intef.es/xsd/ode"
version CDATA #IMPLIED>
```
| Attribute | Required | Type | Description |
|-----------|----------|------|-------------|
| `xmlns` | Fixed | CDATA | Always `http://www.intef.es/xsd/ode` |
| `version` | Optional | CDATA | ODE format version. Currently `"2.0"`. Reserved for future schema bumps. |
Children in declared order:
| Child | Cardinality | Purpose | Subdoc |
|-------|-------------|---------|--------|
| `<userPreferences>` | 0 or 1 | UI-level preferences. Only `theme` is currently emitted. | [metadata.md#userpreferences](metadata.md) |
| `<odeResources>` | 0 or 1 | Package identifiers and runtime version. | [metadata.md#oderesources](metadata.md) |
| `<odeProperties>` | 0 or 1 | Project metadata (`pp_*` keys). | [metadata.md#odeproperties](metadata.md) |
| `<odeNavStructures>` | exactly 1 | Pages, blocks, components (the actual content tree). | [pages-blocks.md](pages-blocks.md) |
Although the DTD allows `userPreferences`, `odeResources`, `odeProperties` to be absent, the modern generator always emits all four. Importers should tolerate absence and fill with defaults from [`metadata-properties.ts`](../../src/shared/export/metadata-properties.ts).
---
## `<userPreferences>` and `<userPreference>`
Stores UI-level preferences. Currently only the active theme is emitted by the generator at [`OdeXmlGenerator.ts:79`](../../src/shared/export/generators/OdeXmlGenerator.ts):
```xml
<userPreferences>
<userPreference>
<key>theme</key>
<value>base</value>
</userPreference>
</userPreferences>
```
| Element | Children | Notes |
|---------|----------|-------|
| `<userPreferences>` | `<userPreference>*` | May be empty. |
| `<userPreference>` | `<key>` + `<value>` | Both `#PCDATA`, XML-escaped. |
Recognized keys (currently single):
| Key | Type | Default | Description |
|-----|------|---------|-------------|
| `theme` | string | `"base"` | Theme folder name. Must match a folder in `theme/` of the same `.elpx`, or be the name of a built-in theme that the runtime can load. See [themes.md](themes.md). |
The fallback `"base"` is hardcoded in the generator (`meta.theme || 'base'`).
---
## `<odeResources>` and `<odeResource>`
Package-level identifiers and runtime version. Generator at [`OdeXmlGenerator.ts:97`](../../src/shared/export/generators/OdeXmlGenerator.ts):
```xml
<odeResources>
<odeResource>
<key>odeId</key>
<value>20251217061325EKESBR</value>
</odeResource>
<odeResource>
<key>odeVersionId</key>
<value>20251217061452XKBQML</value>
</odeResource>
<odeResource>
<key>exe_version</key>
<value>3.0</value>
</odeResource>
</odeResources>
```
| Key | Type | Lifecycle | Description |
|-----|------|-----------|-------------|
| `odeId` | ODE identifier | Stable per project | Persistent project ID. Set once on creation; survives re-saves and re-exports. |
| `odeVersionId` | ODE identifier | Regenerated every export | Snapshot ID. Changes on every save/export; lets two `.elpx` files of the same project be distinguished. |
| `exe_version` | string | Constant per build | The running eXeLearning runtime version. Constant `ODE_VERSION` = `"3.0"` (see [`constants.ts:1000`](../../src/shared/export/constants.ts)). |
ID format and synchronization rules: see [ids.md](ids.md). Other resource keys observed in older fixtures (e.g. `odeVersionName`, `isDownload`, `eXeVersion`) are tolerated by the importer but **not produced** by the modern generator.
---
## `<odeProperties>` and `<odeProperty>`
Project metadata. Each `<odeProperty>` is a `<key>` / `<value>` pair (both `#PCDATA`, XML-escaped). The generator iterates [`METADATA_PROPERTIES`](../../src/shared/export/metadata-properties.ts) and emits one `<odeProperty>` per non-internal entry that has a non-empty value.
```xml
<odeProperties>
<odeProperty>
<key>pp_title</key>
<value>An Introduction to Photosynthesis</value>
</odeProperty>
<odeProperty>
<key>pp_lang</key>
<value>en</value>
</odeProperty>
<odeProperty>
<key>pp_addExeLink</key>
<value>true</value>
</odeProperty>
...
</odeProperties>
```
Boolean values are serialized as the strings `"true"` / `"false"` ([`valueToXmlString`, `metadata-properties.ts:354`](../../src/shared/export/metadata-properties.ts)). HTML/XML payloads (e.g. `pp_extraHeadContent`, `footer`) are XML-escaped via the standard entities — they are NOT wrapped in CDATA inside `<odeProperty>` (CDATA wrapping is reserved for `<htmlView>` and `<jsonProperties>`).
For the full key reference (internal key, XML key, type, default), see [metadata.md](metadata.md). The XSD enumerates the canonical keys at [`ode-content.xsd:255-291`](../../public/app/schemas/ode/ode-content.xsd) (`propertyKeyType`).
---
## `<odeNavStructures>` and the page/block/component tree
The body of the document. A flat list of `<odeNavStructure>` (page) elements, each containing blocks and components. Hierarchy between pages is expressed by `<odeParentPageId>`, **not** by XML nesting.
```dtd
<!ELEMENT odeNavStructures (odeNavStructure*)>
<!ELEMENT odeNavStructure (odePageId, odeParentPageId, pageName, odeNavStructureOrder, odeNavStructureProperties?, odePagStructures?)>
<!ELEMENT odePagStructures (odePagStructure*)>
<!ELEMENT odePagStructure (odePageId, odeBlockId, blockName, iconName?, odePagStructureOrder, odePagStructureProperties?, odeComponents?)>
<!ELEMENT odeComponents (odeComponent*)>
<!ELEMENT odeComponent (odePageId, odeBlockId, odeIdeviceId, odeIdeviceTypeName, htmlView?, jsonProperties?, odeComponentsOrder, odeComponentsProperties?)>
```
The full structural reference is in [pages-blocks.md](pages-blocks.md). This section gives the element-by-element schema.
### `<odeNavStructure>` — a page
```xml
<odeNavStructure>
<odePageId>20251217062007PAGE01</odePageId>
<odeParentPageId/> <!-- empty = root level -->
<pageName>Introduction</pageName>
<odeNavStructureOrder>0</odeNavStructureOrder>
<odeNavStructureProperties>...</odeNavStructureProperties>
<odePagStructures>...</odePagStructures>
</odeNavStructure>
```
| Element | Cardinality | Type | Description |
|---------|-------------|------|-------------|
| `<odePageId>` | 1 | ODE identifier | Unique per page. See [ids.md](ids.md). |
| `<odeParentPageId>` | 1 | ODE identifier or empty | `<odeParentPageId/>` (empty) for root pages; otherwise the `odePageId` of the parent page. |
| `<pageName>` | 1 | string | Display name in navigation. Defaults to `"Page"` when missing. XML-escaped. |
| `<odeNavStructureOrder>` | 1 | integer | Sort key among siblings (0-based in modern generator). |
| `<odeNavStructureProperties>` | 0 or 1 | container | Page-level key/value properties. |
| `<odePagStructures>` | 0 or 1 | container | Blocks contained in the page. |
### `<odeNavStructureProperty>` — page-level property
`<key>` + `<value>` pair. The generator always emits `titlePage` ([`OdeXmlGenerator.ts:166`](../../src/shared/export/generators/OdeXmlGenerator.ts)) and may emit any extra keys that the editor stored on the page object.
| Key | Type | Description |
|-----|------|-------------|
| `titlePage` | string | Heading rendered above the page content (often equal to `<pageName>`). |
| `titleNode` | string | Navigation label override. |
| `titleHtml` | string (HTML) | Custom HTML title. |
| `hidePageTitle` | boolean | If `"true"`, suppress the rendered `<h1>`. |
| `editableInPage` | boolean | Allow inline page-title editing (editor only). |
| `visibility` | boolean | Hide the page in navigation when `"false"`. |
| `highlight` | boolean | Mark the page as highlighted in navigation. |
| `description` | string | Short description shown in tooltips and indices. |
### `<odePagStructure>` — a block (container of components)
```xml
<odePagStructure>
<odePageId>20251217062007PAGE01</odePageId> <!-- redundant but DTD-required -->
<odeBlockId>20251217062007BLK001</odeBlockId>
<blockName>Text</blockName>
<iconName/>
<odePagStructureOrder>0</odePagStructureOrder>
<odePagStructureProperties>...</odePagStructureProperties>
<odeComponents>...</odeComponents>
</odePagStructure>
```
| Element | Cardinality | Type | Description |
|---------|-------------|------|-------------|
| `<odePageId>` | 1 | ODE identifier | Echoes the enclosing page ID. **Must** match the parent `<odeNavStructure>`'s `<odePageId>`. |
| `<odeBlockId>` | 1 | ODE identifier | Unique per block. |
| `<blockName>` | 1 | string | Human label for the block. May be empty. |
| `<iconName>` | 0 or 1 | string | Optional theme icon name. The generator emits `<iconName/>` when absent. |
| `<odePagStructureOrder>` | 1 | integer | Sort key among sibling blocks (0-based). |
| `<odePagStructureProperties>` | 0 or 1 | container | Block-level properties. |
| `<odeComponents>` | 0 or 1 | container | iDevice components in this block. |
Block-level properties emitted by the generator ([`OdeXmlGenerator.ts:213`](../../src/shared/export/generators/OdeXmlGenerator.ts)):
| Key | Type | Description |
|-----|------|-------------|
| `visibility` | boolean | Whether the block is rendered. |
| `teacherOnly` | boolean | Restrict the block to teacher view. |
| `allowToggle` | boolean | Whether users can collapse/expand the block. |
| `minimized` | boolean | Whether the block starts collapsed. |
| `cssClass` | string | Extra CSS class(es) applied to the block container. |
The legacy property `identifier` (custom HTML `id`) is tolerated on import but no longer emitted.
### `<odeComponent>` — an iDevice instance
```xml
<odeComponent>
<odePageId>20251217062007PAGE01</odePageId> <!-- redundant; must match enclosing page -->
<odeBlockId>20251217062007BLK001</odeBlockId> <!-- redundant; must match enclosing block -->
<odeIdeviceId>20251217062007IDEV1</odeIdeviceId>
<odeIdeviceTypeName>text</odeIdeviceTypeName>
<htmlView><![CDATA[<div class="exe-text-template">...</div>]]></htmlView>
<jsonProperties><![CDATA[{"textTextarea":"...","ideviceId":"..."}]]></jsonProperties>
<odeComponentsOrder>0</odeComponentsOrder>
<odeComponentsProperties>
<odeComponentsProperty>
<key>visibility</key>
<value>true</value>
</odeComponentsProperty>
</odeComponentsProperties>
</odeComponent>
```
| Element | Cardinality | Type | Description |
|---------|-------------|------|-------------|
| `<odePageId>` | 1 | ODE identifier | Echoes the enclosing page ID. Must match. |
| `<odeBlockId>` | 1 | ODE identifier | Echoes the enclosing block ID. Must match. |
| `<odeIdeviceId>` | 1 | ODE identifier | Unique per component. |
| `<odeIdeviceTypeName>` | 1 | string (enum) | iDevice type. See [idevices/catalog.md](idevices/catalog.md) for the full list. Legacy CamelCase names are accepted on import and remapped via [`IDEVICE_TYPE_MAP`](../../src/shared/export/constants.ts) at `constants.ts:843`. |
| `<htmlView>` | 0 or 1 | CDATA-wrapped HTML | Pre-rendered HTML for export. See [CDATA rules](#cdata-and-escaping-rules) below. |
| `<jsonProperties>` | 0 or 1 | CDATA-wrapped JSON | Editable state for re-import. See [idevices/patterns.md](idevices/patterns.md) for the four storage patterns. |
| `<odeComponentsOrder>` | 1 | integer | Sort key among siblings (0-based). |
| `<odeComponentsProperties>` | 0 or 1 | container | Component-level properties. |
Component-level properties emitted by the generator ([`OdeXmlGenerator.ts:285`](../../src/shared/export/generators/OdeXmlGenerator.ts)):
| Key | Type | Description |
|-----|------|-------------|
| `visibility` | boolean | Whether the component renders. Always emitted; defaults to `"true"`. |
| `teacherOnly` | boolean | Restrict to teacher view. |
| `cssClass` | string | Extra CSS class(es) applied to the component container. |
> **DTD requirement**: although `<odeComponentsProperties>` is marked optional in the DTD, the generator always emits it (with at least `visibility=true` when no other properties are present). When generating `.elpx` programmatically, emit an empty `<odeComponentsProperties></odeComponentsProperties>` rather than omitting the element — many third-party validators expect it.
---
## CDATA and escaping rules
### Rule 1 — `<htmlView>` and `<jsonProperties>` are ALWAYS CDATA-wrapped
Every emitted `<htmlView>` and `<jsonProperties>` element has its content wrapped in `<![CDATA[ ... ]]>` ([`OdeXmlGenerator.ts:270, 275`](../../src/shared/export/generators/OdeXmlGenerator.ts)). This applies even when the content does not contain any XML-significant characters. Examples:
```xml
<htmlView><![CDATA[<p>Hello world</p>]]></htmlView>
<jsonProperties><![CDATA[{"textTextarea":"<p>Hello world</p>","ideviceId":"20251217062007IDEV1"}]]></jsonProperties>
```
### Rule 2 — `]]>` inside CDATA must be split
CDATA cannot contain the literal sequence `]]>` because it terminates the section. The exporter handles this with [`escapeCdata` (`OdeXmlGenerator.ts:352`](../../src/shared/export/generators/OdeXmlGenerator.ts)):
```js
str.replace(/\]\]>/g, ']]]]><![CDATA[>');
```
When generating `.elpx` by hand or with an LLM, apply the same transform. A paragraph containing the literal text `the operator ]]>` becomes:
```xml
<htmlView><![CDATA[<p>the operator ]]]]><![CDATA[> is rare</p>]]></htmlView>
```
(That is two adjacent CDATA sections; XML parsers concatenate them transparently.)
### Rule 3 — XML attribute and text escaping elsewhere
For all other text nodes (page names, property values, block names, etc.), the generator uses [`escapeXml` (`OdeXmlGenerator.ts:337`](../../src/shared/export/generators/OdeXmlGenerator.ts)):
| Input character | Replaced with |
|-----------------|---------------|
| `&` | `&` |
| `<` | `<` |
| `>` | `>` |
| `"` | `"` |
| `'` | `'` |
Empty / null / undefined values become an empty string.
### Rule 4 — UTF-8 throughout
No BOM, no UTF-16. Non-ASCII characters appear directly in their UTF-8 bytes. Numeric character references are not used.
---
## DTD (bundled inside every `.elpx`)
The DTD is bundled at the ZIP root as `content.dtd`. It is generated from the constant [`ODE_DTD_CONTENT` in `src/shared/export/constants.ts:1006`](../../src/shared/export/constants.ts):
```dtd
<!--
ODE Content DTD
Document Type Definition for eXeLearning ODE XML format (content.xml)
Version: 2.0
Namespace: http://www.intef.es/xsd/ode
Copyright (C) 2025 eXeLearning - License: AGPL-3.0
-->
<!ELEMENT ode (userPreferences?, odeResources?, odeProperties?, odeNavStructures)>
<!ATTLIST ode
xmlns CDATA #FIXED "http://www.intef.es/xsd/ode"
version CDATA #IMPLIED>
<!-- User Preferences -->
<!ELEMENT userPreferences (userPreference*)>
<!ELEMENT userPreference (key, value)>
<!-- ODE Resources -->
<!ELEMENT odeResources (odeResource*)>
<!ELEMENT odeResource (key, value)>
<!-- ODE Properties -->
<!ELEMENT odeProperties (odeProperty*)>
<!ELEMENT odeProperty (key, value)>
<!-- Shared Key-Value Elements -->
<!ELEMENT key (#PCDATA)>
<!ELEMENT value (#PCDATA)>
<!-- Navigation Structures (Pages) -->
<!ELEMENT odeNavStructures (odeNavStructure*)>
<!ELEMENT odeNavStructure (odePageId, odeParentPageId, pageName, odeNavStructureOrder, odeNavStructureProperties?, odePagStructures?)>
<!ELEMENT odePageId (#PCDATA)>
<!ELEMENT odeParentPageId (#PCDATA)>
<!ELEMENT pageName (#PCDATA)>
<!ELEMENT odeNavStructureOrder (#PCDATA)>
<!ELEMENT odeNavStructureProperties (odeNavStructureProperty*)>
<!ELEMENT odeNavStructureProperty (key, value)>
<!-- Block Structures -->
<!ELEMENT odePagStructures (odePagStructure*)>
<!ELEMENT odePagStructure (odePageId, odeBlockId, blockName, iconName?, odePagStructureOrder, odePagStructureProperties?, odeComponents?)>
<!ELEMENT odeBlockId (#PCDATA)>
<!ELEMENT blockName (#PCDATA)>
<!ELEMENT iconName (#PCDATA)>
<!ELEMENT odePagStructureOrder (#PCDATA)>
<!ELEMENT odePagStructureProperties (odePagStructureProperty*)>
<!ELEMENT odePagStructureProperty (key, value)>
<!-- Components (iDevices) -->
<!ELEMENT odeComponents (odeComponent*)>
<!ELEMENT odeComponent (odePageId, odeBlockId, odeIdeviceId, odeIdeviceTypeName, htmlView?, jsonProperties?, odeComponentsOrder, odeComponentsProperties?)>
<!ELEMENT odeIdeviceId (#PCDATA)>
<!ELEMENT odeIdeviceTypeName (#PCDATA)>
<!ELEMENT htmlView (#PCDATA)>
<!ELEMENT jsonProperties (#PCDATA)>
<!ELEMENT odeComponentsOrder (#PCDATA)>
<!ELEMENT odeComponentsProperties (odeComponentsProperty*)>
<!ELEMENT odeComponentsProperty (key, value)>
```
A canonical, more heavily commented version of the same DTD lives in the repository at [`public/app/schemas/ode/content.dtd`](../../public/app/schemas/ode/content.dtd). The two are equivalent in element structure; only the comments differ.
---
## XSD (companion schema, not bundled)
A stricter XML Schema is available at [`public/app/schemas/ode/ode-content.xsd`](../../public/app/schemas/ode/ode-content.xsd). Use it when you need stronger validation than the DTD provides.
### What the XSD adds beyond the DTD
| Feature | XSD constraint | DTD equivalent |
|---------|----------------|----------------|
| Identifier format | `xs:pattern value="[0-9]{14}[A-Z0-9]{6}\|page-[a-z0-9-]+\|[a-zA-Z0-9_-]+"` on `odeIdentifierType` | None — `#PCDATA` |
| iDevice type list | `xs:enumeration` of every modern + legacy/FPD type name (`ideviceTypeType`) | None — `#PCDATA` |
| Property key list | `xs:enumeration` of recognized `pp_*` keys (`propertyKeyType`) | None — `#PCDATA` |
| Boolean strings | `xs:enumeration` `"true"`, `"false"`, `"True"`, `"False"` (`booleanStringType`) | None |
| Order numbers | `xs:integer` for `*Order` elements | `#PCDATA` |
| Required `odeNavStructure` | `minOccurs="1"` | None — DTD allows zero |
The XSD is intentionally not bundled inside `.elpx` because (a) DTDs are still the de-facto standard that many lightweight validators understand, and (b) keeping the bundled schema small saves bytes.
### Validating with the XSD
```bash
xmllint --noout --schema public/app/schemas/ode/ode-content.xsd path/to/content.xml
```
See [validation.md](validation.md) for the full validation playbook.
---
## Cardinality and ordering summary
A compact reference for code generators:
```
ode
├── userPreferences? (0..1)
│ └── userPreference*
│ ├── key
│ └── value
├── odeResources? (0..1)
│ └── odeResource*
│ ├── key
│ └── value
├── odeProperties? (0..1)
│ └── odeProperty*
│ ├── key
│ └── value
└── odeNavStructures (1..1) <-- the only required top-level child
└── odeNavStructure*
├── odePageId (1)
├── odeParentPageId (1) <!-- empty for root pages -->
├── pageName (1)
├── odeNavStructureOrder (1)
├── odeNavStructureProperties?
│ └── odeNavStructureProperty*
│ ├── key
│ └── value
└── odePagStructures?
└── odePagStructure*
├── odePageId (1) <!-- redundant -->
├── odeBlockId (1)
├── blockName (1)
├── iconName? (0..1)
├── odePagStructureOrder (1)
├── odePagStructureProperties?
│ └── odePagStructureProperty*
│ ├── key
│ └── value
└── odeComponents?
└── odeComponent*
├── odePageId (1)
├── odeBlockId (1)
├── odeIdeviceId (1)
├── odeIdeviceTypeName (1)
├── htmlView? (0..1) CDATA-wrapped
├── jsonProperties? (0..1) CDATA-wrapped
├── odeComponentsOrder (1)
└── odeComponentsProperties?
└── odeComponentsProperty*
├── key
└── value
```
Element order is **strict**: a parser MUST reject an `<odeComponent>` whose `<htmlView>` appears after its `<odeComponentsOrder>`, or an `<odeNavStructure>` that places `<pageName>` before `<odeParentPageId>`.
---
## Common gotchas
1. **Redundant IDs must be in lockstep.** `<odePageId>` and `<odeBlockId>` appear at every nesting level (page, block, component). All three references inside an `<odeComponent>` MUST agree with the IDs of the enclosing `<odePagStructure>` and `<odeNavStructure>`. Mismatches make the importer reject the component or attach it to the wrong block.
2. **Boolean strings only.** Properties typed as boolean in [`metadata-properties.ts`](../../src/shared/export/metadata-properties.ts) are emitted as the literal strings `"true"` / `"false"`. Do not use `1`/`0`, `True`/`False`, or `yes`/`no`. The XSD `booleanStringType` is permissive on capitalization, but the importer's `parsePropertyValue` (`metadata-properties.ts:333`) only checks `toLowerCase() === 'true'`.
3. **Always emit `<odeComponentsProperties>` even when empty.** The DTD marks it optional, but every reference fixture and the generator always emit it (with at least `visibility=true`). Some third-party tools assume its presence.
4. **CDATA wrappers are unconditional** for `<htmlView>` and `<jsonProperties>`. Do not "optimize" by emitting raw HTML-escaped content — the importer's regex-based extractors expect CDATA delimiters.
5. **Page hierarchy is flat in XML.** Nesting one `<odeNavStructure>` inside another's `<odePagStructures>` is invalid. Use `<odeParentPageId>` to express parent-child relationships.
6. **`exe_version`, not `eXeVersion`.** The XML key for the runtime version is `exe_version` (snake_case). The misspelling `eXeVersion` appears in some old fixtures; the importer accepts both, but the modern generator only writes `exe_version`.
7. **Asset references use `{{context_path}}`.** Inside `<htmlView>` and `<jsonProperties>`, asset URLs take the v4 form `{{context_path}}/<exportPath>` where `<exportPath>` is `<folderPath>/<filename>` for assets inside a user-created folder (e.g. `photos/vacation/sunset.jpg`) or just `<filename>` when no folder. The placeholder absorbs both the page-depth `../` prefix and the `content/resources/` segment. The legacy long form `{{context_path}}/content/resources/<exportPath>` is also accepted on import. The asset itself lives at `content/resources/<exportPath>` inside the ZIP — never inside a legacy v3 per-asset UUID subfolder (`[0-9]{14}[A-Z0-9]{6}`). The import pipeline rewrites either reference form to `asset://<uuid>`. See [assets.md](assets.md).
---
## See also
- [container.md](container.md) — every file inside an `.elpx` ZIP
- [ids.md](ids.md) — ODE identifier format, lifecycle, and synchronization rules
- [metadata.md](metadata.md) — full reference for `pp_*` keys, `userPreferences`, `odeResources`
- [pages-blocks.md](pages-blocks.md) — flat-list navigation model, internal `exe-node:` links
- [idevices/catalog.md](idevices/catalog.md) — every iDevice type
- [idevices/patterns.md](idevices/patterns.md) — the four content-storage patterns
- [validation.md](validation.md) — validating with `xmllint` and `OdeXmlValidator`
- [ai-generation.md](ai-generation.md) — rules for LLMs producing `.elpx`
- [examples/minimal-content-xml.md](examples/minimal-content-xml.md) — a complete, validating example
# ===== doc/elpx-format/ids.md =====
# ELPX Identifiers
This document describes every identifier used in a `content.xml` file: their format, generation, lifecycle, synchronization rules, and what happens to them during import.
See also: [ELPX format overview](../elpx-format.md) | [Container](container.md) | [Pages and blocks](pages-blocks.md) | [Metadata](metadata.md)
---
## ID format
Modern ODE identifiers are produced by `generateOdeId()` in `OdeXmlGenerator.ts:315–332`:
```
YYYYMMDDHHmmss + 6 chars from [A-Z0-9]
```
**Example:** `20251125215856KTWCLS`
- Characters 1–14: UTC wall-clock timestamp, zero-padded (`YYYY`, `MM`, `DD`, `HH`, `mm`, `ss`).
- Characters 15–20: six characters drawn uniformly at random from the 36-character alphabet `ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789`.
This gives the ID two useful properties: it is lexicographically time-sortable (earlier IDs sort before later ones), and the 6-character random suffix makes collisions extremely unlikely even when many IDs are generated within the same second.
```typescript
// OdeXmlGenerator.ts:315–332
export function generateOdeId(): string {
const now = new Date();
const timestamp =
now.getFullYear().toString() +
String(now.getMonth() + 1).padStart(2, '0') +
String(now.getDate()).padStart(2, '0') +
String(now.getHours()).padStart(2, '0') +
String(now.getMinutes()).padStart(2, '0') +
String(now.getSeconds()).padStart(2, '0');
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
let random = '';
for (let i = 0; i < 6; i++) {
random += chars.charAt(Math.floor(Math.random() * chars.length));
}
return timestamp + random;
}
```
---
## XSD `odeIdentifierType` pattern
The XML Schema (`ode-content.xsd:148–152`) defines a union pattern for all ID attributes:
```
[0-9]{14}[A-Z0-9]{6}|page-[a-z0-9-]+|[a-zA-Z0-9_-]+
```
The three alternations exist for distinct historical reasons:
| Alternation | Example | Reason |
|---|---|---|
| `[0-9]{14}[A-Z0-9]{6}` | `20251125215856KTWCLS` | Modern format produced by `generateOdeId()`. |
| `page-[a-z0-9-]+` | `page-introduction` | Slug-style IDs used in older eXeLearning 3.x imports where page IDs were derived from page titles. |
| `[a-zA-Z0-9_-]+` | `idevice-3` or `page-4` | Generic legacy IDs from Python eXeLearning 2.x (`.elp` files) where pages and iDevices had small sequential integers as identifiers. |
All three forms are accepted by the importer. The modern generator always produces the first form.