Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
228915e
Add initial H5P export support
erseco Jun 20, 2025
95985ec
Add H5P export target to Makefile
erseco Jun 20, 2025
dcca89c
Fix Makefile target indentation
erseco Jun 20, 2025
52d5c71
Refine H5P export
erseco Jun 20, 2025
a90bd82
Improve H5P export
erseco Jun 20, 2025
15ff9d7
Use H5P core helper and add dependency
erseco Jun 20, 2025
522e567
Refine H5P export command
erseco Jun 20, 2025
574cabf
Fix Makefile tabs for H5P export
erseco Jun 20, 2025
6fc61d2
Fix Makefile help by tab-indenting H5P rule
erseco Jun 20, 2025
ffb6244
Fix H5P package directories
erseco Jun 20, 2025
0e8e735
Fix H5P library json structure
erseco Jun 20, 2025
ac7f9aa
test: add H5P export command test
erseco Jun 20, 2025
f4e511e
Revert "Bump symfony/serializer from 7.3.3 to 7.3.4 (#333)"
erseco Oct 6, 2025
290a474
Merge branch 'main' of github.com:exelearning/exelearning
erseco Oct 6, 2025
3af8f5b
Merge branch 'main' of github.com:exelearning/exelearning
erseco Oct 7, 2025
cc38e3b
Merge branch 'main' of github.com:exelearning/exelearning
erseco Oct 8, 2025
6db24b7
Merge branch 'main' of github.com:exelearning/exelearning
erseco Oct 9, 2025
ac4a70d
Merge branch 'main' of github.com:exelearning/exelearning
erseco Oct 9, 2025
36fadcd
Inital version
erseco Oct 9, 2025
9659252
More changes
erseco Oct 9, 2025
e7a1dfd
More changes
erseco Oct 9, 2025
e2b967a
Experimental export to h5p
erseco Oct 9, 2025
fe2527b
Experimental export to h5p
erseco Oct 9, 2025
d1e4553
Merge branch 'main' of github.com:exelearning/exelearning
erseco Oct 9, 2025
5a96347
Merge branch 'main' of github.com:exelearning/exelearning into featur…
erseco Oct 9, 2025
7d34486
Fix lint issues
erseco Oct 9, 2025
68d4e63
Fix prama sqlite settings for tests
erseco Oct 9, 2025
bf61187
Merge branch 'hotfix/fix-pragma-on-test-and-deprecation' of github.co…
erseco Oct 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,24 @@ export-elp-epub3:
export-elp-ims:
@$(MAKE) export-elp FORMAT=ims INPUT="$(INPUT)" OUTPUT="$(OUTPUT)" DEBUG="$(DEBUG)" BASE_URL="$(BASE_URL)"

export-elp-h5p:
ifndef INPUT
$(error INPUT is required. Use INPUT=/absolute/path/to/file.elp)
endif
ifndef OUTPUT
$(error OUTPUT is required. Use OUTPUT=/absolute/path/to/output.h5p)
endif
$(eval EXPANDED_INPUT := $(call EXPAND_PATH,$(INPUT)))
@if [ ! -f "$(EXPANDED_INPUT)" ]; then \
echo "❌ INPUT file does not exist: $(EXPANDED_INPUT)"; \
exit 1; \
fi
@mkdir -p $(dir $(OUTPUT))
$(eval TMP_DIR := $(OUTPUT).tmpdir)
@$(MAKE) export-elp FORMAT=h5p INPUT="$(EXPANDED_INPUT)" OUTPUT="$(TMP_DIR)" DEBUG="$(DEBUG)" BASE_URL="$(BASE_URL)"
@mv "$(TMP_DIR)"/*.h5p "$(OUTPUT)"
@rm -rf "$(TMP_DIR)"


# Install nativephp/php-bin package temporarily without modifying composer.json
install-php-bin:
Expand Down Expand Up @@ -558,6 +576,7 @@ help:
@echo " export-elp-scorm2004 - Export .elp to SCORM 2004 format (alias for FORMAT=scorm2004)"
@echo " export-elp-ims - Export .elp to IMS format (alias for FORMAT=ims)"
@echo " export-elp-epub3 - Export .elp to EPUB 3 format (alias for FORMAT=epub3)"
@echo " export-elp-h5p - Export .elp to H5P (experimental) format (alias for FORMAT=h5p)"
@echo " export-elp-elp - Re-export .elp file (alias for FORMAT=elp)"
@echo ""
@echo "Data:"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ This version is built with modern technologies (PHP 8, Symfony 7) and provides a
* Creation and edition of interactive educational content
* Multiple iDevices (interactive elements)
* Multilingual support
* Exportation to various formats
* Exportation to multiple formats (SCORM, IMS, EPUB, HTML and H5P)
* Moodle integration
* [RESTful API](./doc/development/rest-api.md) Self-documented with Swagger
* Real-time collaborative features powered by [Mercure](https://mercure.rocks/)
Expand Down
18 changes: 14 additions & 4 deletions doc/development/environment.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,10 +136,20 @@ The project provides a Makefile to simplify common tasks:
| `make test` | Run unit tests |
| `make lint` | Check PHP code style |
| `make fix` | Automatically fix code style issues |
| `make update` | Update Composer dependencies |
| `make translations` | Update translation files |

### Installing `make`
| `make update` | Update Composer dependencies |
| `make translations` | Update translation files |

### Exporting Content

Use the Makefile to generate packages from `.elp` projects. For example, to create an experimental H5P package:

```bash
make export-elp-h5p INPUT=/path/project.elp OUTPUT=/path/project.h5p
```

Other formats can be exported by replacing `h5p` with `html5`, `scorm12`, `scorm2004`, `ims`, or `epub3`.

### Installing `make`

On Linux, `make` is usually pre-installed. On Windows, install it with:

Expand Down
1 change: 1 addition & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@

<extensions>
<bootstrap class="Symfony\Component\Panther\ServerExtension"/>
<bootstrap class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>
</extensions>

<testsuites>
Expand Down
108 changes: 105 additions & 3 deletions public/app/workarea/menus/navbar/items/navbarFile.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ export default class NavbarFile {
this.exportEPUB3AsButton = this.menu.navbar.querySelector(
'#navbar-button-exportas-epub3'
);
this.exportH5PButton = this.menu.navbar.querySelector(
'#navbar-button-export-h5p'
);
this.exportXmlPropertiesButton = this.menu.navbar.querySelector(
'#navbar-button-export-xml-properties'
);
Expand Down Expand Up @@ -127,6 +130,7 @@ export default class NavbarFile {
this.setExportIMSEvent();
this.setExportIMSAsEvent();
this.setExportEPUB3Event();
this.setExportH5PEvent();
this.setExportEPUB3AsEvent();
this.setExportXmlPropertiesEvent();
this.setExportXmlPropertiesAsEvent();
Expand Down Expand Up @@ -439,6 +443,14 @@ export default class NavbarFile {
});
}

setExportH5PEvent() {
if (!this.exportH5PButton) return;
this.exportH5PButton.addEventListener('click', () => {
if (eXeLearning.app.project.checkOpenIdevice()) return;
this.exportH5PEvent();
});
}

setExportEPUB3AsEvent() {
if (!this.exportEPUB3AsButton) return;
this.exportEPUB3AsButton.addEventListener('click', () => {
Expand Down Expand Up @@ -1207,7 +1219,6 @@ export default class NavbarFile {
}

/**
<<<<<<< HEAD
* Export Website to folder (unzipped) — offline Electron only
*/
async exportHTML5FolderAsEvent() {
Expand Down Expand Up @@ -1285,8 +1296,6 @@ export default class NavbarFile {
}

/**
=======
>>>>>>> c0ba7aea408904076081df962baf800d79424a91
* Export the ode as HTML5 and download it
*
*/
Expand Down Expand Up @@ -1782,6 +1791,99 @@ export default class NavbarFile {
eXeLearning.app.interface.connectionTime.loadLasUpdatedInInterface();
}

async exportH5PEvent() {
let toastData = {
title: _('Export'),
body: _('Generating export files...'),
icon: 'downloading',
};
let toast = eXeLearning.app.toasts.createToast(toastData);
let odeSessionId = eXeLearning.app.project.odeSession;
let response = await eXeLearning.app.api.getOdeExportDownload(
odeSessionId,
'h5p'
);
if (response['responseMessage'] == 'OK') {
this.downloadLink(
response['urlZipFile'],
response['exportProjectName']
);
toast.toastBody.innerHTML = _('The project has been exported.');
} else {
toast.toastBody.innerHTML = _(
'An error occurred while exporting the project.'
);
toast.toastBody.classList.add('error');
eXeLearning.app.modals.alert.show({
title: _('Error'),
body: response['responseMessage']
? response['responseMessage']
: _('Unknown error.'),
contentId: 'error',
});
}

setTimeout(() => {
toast.remove();
}, 1000);

eXeLearning.app.interface.connectionTime.loadLasUpdatedInInterface();
}

/**
* Export ePub3 (Save As...)
*/
async exportEPUB3AsEvent() {
let toastData = {
title: _('Export'),
body: _('Generating export files...'),
icon: 'downloading',
};
let toast = eXeLearning.app.toasts.createToast(toastData);
let odeSessionId = eXeLearning.app.project.odeSession;
let response = await eXeLearning.app.api.getOdeExportDownload(
odeSessionId,
'epub3'
);
if (response['responseMessage'] == 'OK') {
const url = response['urlZipFile'];
const suggested = this.normalizeSuggestedName(
response['exportProjectName'],
'export-epub3'
);
const keyBase = window.__currentProjectId || 'default';
if (
window.electronAPI &&
typeof window.electronAPI.saveAs === 'function'
) {
await window.electronAPI.saveAs(
url,
`${keyBase}:export-epub3`,
suggested
);
} else {
this.downloadLink(url, suggested);
}
toast.toastBody.innerHTML = _('The project has been exported.');
} else {
toast.toastBody.innerHTML = _(
'An error occurred while exporting the project.'
);
toast.toastBody.classList.add('error');
eXeLearning.app.modals.alert.show({
title: _('Error'),
body: response['responseMessage']
? response['responseMessage']
: _('Unknown error.'),
contentId: 'error',
});
}
setTimeout(() => {
toast.remove();
}, 1000);
eXeLearning.app.interface.connectionTime.loadLasUpdatedInInterface();
}

/**
* Export ePub3 (Save As...)
*/
Expand Down
3 changes: 2 additions & 1 deletion src/Command/net/exelearning/Command/ElpExportCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ protected function configure(): void
$this
->addArgument('input', InputArgument::REQUIRED, 'Input ELP file path (use "-" for stdin)')
->addArgument('output', InputArgument::REQUIRED, 'Output directory')
->addArgument('format', InputArgument::OPTIONAL, 'Export format (elp, html5, html5-sp, scorm12, scorm2004, ims, epub3)', $this->defaultFormat)
->addArgument('format', InputArgument::OPTIONAL, 'Export format (elp, html5, html5-sp, scorm12, scorm2004, ims, epub3, h5p)', $this->defaultFormat)
->addOption('debug', 'd', InputOption::VALUE_NONE, 'Enable debug mode')
->addOption('base-url', 'b', InputOption::VALUE_OPTIONAL, 'Base URL for links', false);
}
Expand All @@ -67,6 +67,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
Constants::EXPORT_TYPE_SCORM2004,
Constants::EXPORT_TYPE_IMS,
Constants::EXPORT_TYPE_EPUB3,
Constants::EXPORT_TYPE_H5P,
];

if (!in_array($format, $validFormats, true)) {
Expand Down
15 changes: 15 additions & 0 deletions src/Command/net/exelearning/Command/ElpExportH5pCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace App\Command\net\exelearning\Command;

use App\Constants;
use Symfony\Component\Console\Attribute\AsCommand;

#[AsCommand(
name: 'elp:export-h5p',
description: 'Export ELP to H5P format',
)]
class ElpExportH5pCommand extends ElpExportCommand
{
protected string $defaultFormat = Constants::EXPORT_TYPE_H5P;
}
3 changes: 3 additions & 0 deletions src/Constants.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,13 +127,15 @@ class Constants
public const EXPORT_TYPE_SCORM2004 = 'scorm2004';
public const EXPORT_TYPE_IMS = 'ims';
public const EXPORT_TYPE_EPUB3 = 'epub3';
public const EXPORT_TYPE_H5P = 'h5p';

// Export types filename suffixes
public const SUFFIX_TYPE_HTML5 = '_web';
public const SUFFIX_TYPE_HTML5_SP = '_page';
public const SUFFIX_TYPE_SCORM12 = '_scorm';
public const SUFFIX_TYPE_SCORM2004 = '_scorm2004';
public const SUFFIX_TYPE_IMS = '_ims';
public const SUFFIX_TYPE_H5P = '_h5p';

// Export files
public const EXPORT_FILE_INDEX_NAME = 'index';
Expand Down Expand Up @@ -229,6 +231,7 @@ class Constants
public const FILE_EXTENSION_ELP = 'elp';
public const FILE_EXTENSION_ZIP = 'zip';
public const FILE_EXTENSION_EPUB = 'epub';
public const FILE_EXTENSION_H5P = 'h5p';
public const FILE_EXTENSION_XML = 'xml';

// CSV separator
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -451,7 +451,7 @@ private function publishOdeBlockStatusEvent(
?string $actionType,
string $userEmail,
?string $odeComponentFlag = null,
?string $timeIdeviceEditing,
?string $timeIdeviceEditing = null,
?string $pageId = null, // Collaborative
): void {
$this->publish(
Expand Down
49 changes: 37 additions & 12 deletions src/Doctrine/Middleware/SqlitePragmaMiddleware.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,18 +28,21 @@ public function connect(
return $connection;
}

// WAL keeps readers and writers from blocking each other.
$connection->exec('PRAGMA journal_mode = WAL');
// NORMAL sync trims redundant fsyncs without losing crash safety on checkpoints.
$connection->exec('PRAGMA synchronous = NORMAL');
// Retries for ~5s curb transient "database is locked" errors when writers race.
$connection->exec('PRAGMA busy_timeout = 5000');
// Doctrine disables foreign keys by default for SQLite; force them back on.
$connection->exec('PRAGMA foreign_keys = ON');
// Temp tables and sorting spill to memory to avoid disk-backed temp files.
$connection->exec('PRAGMA temp_store = MEMORY');
// Negative cache size sets ~4MB memory caching for hot pages (no disk persistence).
$connection->exec('PRAGMA cache_size = -4000');
// Skip WAL and disk-related PRAGMAs for pure in-memory DBs.
if (!$this->isMemory($params)) {
// WAL keeps readers and writers from blocking each other.
$connection->exec('PRAGMA journal_mode = WAL');
// NORMAL sync trims redundant fsyncs without losing crash safety on checkpoints.
$connection->exec('PRAGMA synchronous = NORMAL');
// Retries for ~5s curb transient "database is locked" errors when writers race.
$connection->exec('PRAGMA busy_timeout = 5000');
// Doctrine disables foreign keys by default for SQLite; force them back on.
$connection->exec('PRAGMA foreign_keys = ON');
// Temp tables and sorting spill to memory to avoid disk-backed temp files.
$connection->exec('PRAGMA temp_store = MEMORY');
// Negative cache size sets ~4MB memory caching for hot pages (no disk persistence).
$connection->exec('PRAGMA cache_size = -4000');
}

return $connection;
}
Expand Down Expand Up @@ -71,6 +74,28 @@ private function isSqlite(array $params): bool

return false;
}

/**
* Detects in-memory SQLite (":memory:" or memory param).
*
* @param array<string, mixed> $params
*/
private function isMemory(array $params): bool
{
if (!empty($params['memory'])) {
return true;
}
$url = $params['url'] ?? null;
if (\is_string($url) && \str_contains($url, ':memory:')) {
return true;
}
$path = $params['path'] ?? null;
if (\is_string($path) && ':memory:' === $path) {
return true;
}

return false;
}
};
}
}
Loading
Loading