execonvert 0.5.4 → 0.5.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/dist/cli/cli/execonvert.js +31 -5
- package/dist/cli/src/converter.js +30 -6
- package/dist/cli/src/docx-import.js +2 -1
- package/dist/cli/src/exe-runtime.js +29 -0
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,22 @@ Categorías usadas: **Añadido**, **Cambiado**, **Corregido**, **Eliminado**.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.5.5] - 2026-09-05
|
|
13
|
+
|
|
14
|
+
### Añadido
|
|
15
|
+
- `execonvert inspect` muestra la versión de eXeLearning con la que se creó el
|
|
16
|
+
proyecto, y la incluye en su salida `--json` como `exeVersion`.
|
|
17
|
+
- Aviso al convertir un `.elpx` creado con una versión de eXeLearning más
|
|
18
|
+
reciente que la que incorpora eXeConvert: la conversión sigue adelante, pero
|
|
19
|
+
puede no reconocer los elementos nuevos. Los proyectos de versiones iguales o
|
|
20
|
+
anteriores no muestran nada, porque funcionan.
|
|
21
|
+
|
|
22
|
+
### Corregido
|
|
23
|
+
- Los `.elpx` generados declaraban `exe_version` 3.0, un valor heredado de la
|
|
24
|
+
plantilla, cuando el runtime incorporado es el 4.0.3. Ahora declaran la
|
|
25
|
+
versión real, que es la que escribe eXeLearning en sus propios archivos. El
|
|
26
|
+
campo es informativo: nada en el runtime lo usa para tomar decisiones.
|
|
27
|
+
|
|
12
28
|
## [0.5.4] - 2026-09-05
|
|
13
29
|
|
|
14
30
|
### Corregido
|
|
@@ -3,16 +3,17 @@ import { globSync } from 'node:fs';
|
|
|
3
3
|
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { stderr, stdout } from 'node:process';
|
|
5
5
|
import { createI18n } from '../src/i18n.js';
|
|
6
|
-
import { buildPrintableHtmlDocument, buildPdfBlobFromPrintableHtml, convertElpxToDocx, convertElpxToHtml, convertHtmlToDocxResult, inspectElpxPages, } from '../src/converter.js';
|
|
6
|
+
import { buildPrintableHtmlDocument, buildPdfBlobFromPrintableHtml, convertElpxToDocx, convertElpxToHtml, convertHtmlToDocxResult, inspectElpx, inspectElpxPages, } from '../src/converter.js';
|
|
7
7
|
import { convertDocxToElpx } from '../src/docx-import.js';
|
|
8
8
|
import { convertElpxToMarkdown } from '../src/elpx-markdown.js';
|
|
9
9
|
import { convertElpToElpx } from '../src/legacy-elp.js';
|
|
10
10
|
import { convertMarkdownToElpx } from '../src/markdown-import.js';
|
|
11
11
|
import { installCliRuntime } from './runtime.js';
|
|
12
|
+
import { EXE_RUNTIME_VERSION, isNewerThanRuntime } from '../src/exe-runtime.js';
|
|
12
13
|
import { automaticCheck, autoCheckAllowed, runUpdate, updateText } from './updates.js';
|
|
13
|
-
const CLI_VERSION = '0.5.
|
|
14
|
+
const CLI_VERSION = '0.5.5'.startsWith('__')
|
|
14
15
|
? String(JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version)
|
|
15
|
-
: '0.5.
|
|
16
|
+
: '0.5.5';
|
|
16
17
|
const cliMessages = {
|
|
17
18
|
es: {
|
|
18
19
|
'help.title': 'CLI de eXeConvert',
|
|
@@ -32,6 +33,7 @@ const cliMessages = {
|
|
|
32
33
|
'help.output.markdown': 'desde .md/.txt: .elpx',
|
|
33
34
|
'help.option.to': 'Formato de salida para conversión múltiple (elpx, docx, md, pdf). Los archivos no compatibles se ignoran.',
|
|
34
35
|
'warn.skippedInput': 'Aviso: se ignora {file}: no se puede convertir a {format}.',
|
|
36
|
+
'warn.newerProject': 'Aviso: el proyecto se creó con eXeLearning {version} y esta versión incorpora la {runtime}. La conversión puede no reconocer los elementos nuevos.',
|
|
35
37
|
'help.option.outDir': 'Directorio de salida (por defecto: mismo directorio que cada entrada)',
|
|
36
38
|
'help.option.json': 'Imprime JSON legible por máquina',
|
|
37
39
|
'help.option.pages': 'Exporta solo las referencias de página seleccionadas de un .elpx',
|
|
@@ -81,6 +83,7 @@ const cliMessages = {
|
|
|
81
83
|
'help.output.markdown': 'des de .md/.txt: .elpx',
|
|
82
84
|
'help.option.to': 'Format de sortida per a conversió múltiple (elpx, docx, md, pdf). Els fitxers no compatibles s’ignoren.',
|
|
83
85
|
'warn.skippedInput': 'Avís: s’ignora {file}: no es pot convertir a {format}.',
|
|
86
|
+
'warn.newerProject': 'Avís: el projecte s’ha creat amb eXeLearning {version} i aquesta versió incorpora la {runtime}. La conversió pot no reconèixer els elements nous.',
|
|
84
87
|
'help.option.outDir': 'Directori de sortida (per defecte: mateix directori que cada entrada)',
|
|
85
88
|
'help.option.json': 'Imprimeix JSON llegible per màquines',
|
|
86
89
|
'help.option.pages': 'Exporta només les referències de pàgina seleccionades d’un .elpx',
|
|
@@ -130,6 +133,7 @@ const cliMessages = {
|
|
|
130
133
|
'help.output.markdown': 'from .md/.txt: .elpx',
|
|
131
134
|
'help.option.to': 'Output format for batch conversion (elpx, docx, md, pdf). Incompatible files are skipped.',
|
|
132
135
|
'warn.skippedInput': 'Warning: skipping {file}: it cannot be converted to {format}.',
|
|
136
|
+
'warn.newerProject': 'Warning: this project was created with eXeLearning {version} and this build carries {runtime}. The conversion may not recognise its newer elements.',
|
|
133
137
|
'help.option.outDir': 'Output directory (default: same directory as each input)',
|
|
134
138
|
'help.option.json': 'Print machine-readable JSON',
|
|
135
139
|
'help.option.pages': 'Export only selected page refs from an .elpx input',
|
|
@@ -548,11 +552,15 @@ function printInspectText(pages) {
|
|
|
548
552
|
async function runInspect(inputPath, json) {
|
|
549
553
|
installCliRuntime();
|
|
550
554
|
const inputFile = await readInputFile(inputPath, buildMime('elpx'));
|
|
551
|
-
const
|
|
555
|
+
const inspection = await inspectElpx(inputFile);
|
|
556
|
+
const pages = buildPageRefs(inspection.pages);
|
|
552
557
|
if (json) {
|
|
553
|
-
stdout.write(`${JSON.stringify({ input: resolve(inputPath), pages }, null, 2)}\n`);
|
|
558
|
+
stdout.write(`${JSON.stringify({ input: resolve(inputPath), exeVersion: inspection.exeVersion, pages }, null, 2)}\n`);
|
|
554
559
|
return;
|
|
555
560
|
}
|
|
561
|
+
if (inspection.exeVersion) {
|
|
562
|
+
stdout.write(`eXeLearning: ${inspection.exeVersion}\n`);
|
|
563
|
+
}
|
|
556
564
|
printInspectText(pages);
|
|
557
565
|
}
|
|
558
566
|
async function runConvert(args) {
|
|
@@ -563,6 +571,9 @@ async function runConvert(args) {
|
|
|
563
571
|
const inputFormat = detectFormat(inputPath, i18n.t);
|
|
564
572
|
const outputFormat = detectFormat(outputPath, i18n.t);
|
|
565
573
|
const inputFile = await readInputFile(inputPath, buildMime(inputFormat));
|
|
574
|
+
if (inputFormat === 'elpx' && !args.json) {
|
|
575
|
+
await warnIfNewerThanRuntime(inputFile, i18n.t);
|
|
576
|
+
}
|
|
566
577
|
const selectedPageIds = inputFormat === 'elpx'
|
|
567
578
|
? await loadSelectedPageIds(inputFile, args.pages, args.pageIds)
|
|
568
579
|
: [];
|
|
@@ -713,6 +724,21 @@ async function runConvert(args) {
|
|
|
713
724
|
}
|
|
714
725
|
throw new Error(i18n.t('error.unsupportedConversion', { input: inputFormat, output: outputFormat }));
|
|
715
726
|
}
|
|
727
|
+
// Only newer projects are worth a warning: older ones convert fine, as every
|
|
728
|
+
// release from 4.0.0 on has shown. A project from a release this build does not
|
|
729
|
+
// know may carry elements the conversion drops without saying anything.
|
|
730
|
+
async function warnIfNewerThanRuntime(inputFile, t) {
|
|
731
|
+
try {
|
|
732
|
+
const { exeVersion } = await inspectElpx(inputFile);
|
|
733
|
+
if (isNewerThanRuntime(exeVersion)) {
|
|
734
|
+
stderr.write(`${t('warn.newerProject', { version: exeVersion, runtime: EXE_RUNTIME_VERSION })}\n`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
catch {
|
|
738
|
+
// Inspecting is only for the warning: a project that cannot be read here
|
|
739
|
+
// will fail with its own message further down.
|
|
740
|
+
}
|
|
741
|
+
}
|
|
716
742
|
async function loadSelectedPageIds(inputFile, pageRefs, pageIds) {
|
|
717
743
|
if (pageRefs.length === 0 && pageIds.length === 0) {
|
|
718
744
|
return [];
|
|
@@ -1830,15 +1830,26 @@ function ensurePdfMakeFonts() {
|
|
|
1830
1830
|
pdfMakeInitialized = true;
|
|
1831
1831
|
}
|
|
1832
1832
|
export async function inspectElpxPages(file) {
|
|
1833
|
+
return (await inspectElpx(file)).pages;
|
|
1834
|
+
}
|
|
1835
|
+
/**
|
|
1836
|
+
* Reads the pages and the eXeLearning release the project declares. The version
|
|
1837
|
+
* is what tells a caller whether the project comes from a release newer than the
|
|
1838
|
+
* vendored runtime, in which case the conversion may not know every element.
|
|
1839
|
+
*/
|
|
1840
|
+
export async function inspectElpx(file) {
|
|
1833
1841
|
const input = new Uint8Array(await file.arrayBuffer());
|
|
1834
1842
|
const entries = unzipSync(input);
|
|
1835
1843
|
const project = parseProject(entries);
|
|
1836
|
-
return
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1844
|
+
return {
|
|
1845
|
+
exeVersion: project.exeVersion,
|
|
1846
|
+
pages: project.pages.map(page => ({
|
|
1847
|
+
id: page.id,
|
|
1848
|
+
parentId: page.parentId,
|
|
1849
|
+
title: page.title,
|
|
1850
|
+
depth: page.depth,
|
|
1851
|
+
})),
|
|
1852
|
+
};
|
|
1842
1853
|
}
|
|
1843
1854
|
async function buildCompatibleDocx(htmlDocument) {
|
|
1844
1855
|
const htmlDoc = new DOMParser().parseFromString(htmlDocument, 'text/html');
|
|
@@ -1958,6 +1969,7 @@ function parseProject(entries) {
|
|
|
1958
1969
|
if (parserError) {
|
|
1959
1970
|
throw new Error('El content.xml no se ha podido interpretar correctamente.');
|
|
1960
1971
|
}
|
|
1972
|
+
const exeVersion = findResourceValue(xmlDoc, 'exe_version');
|
|
1961
1973
|
const title = findPropertyValue(xmlDoc, 'pp_title') || 'eXeLearning';
|
|
1962
1974
|
const subtitle = findPropertyValue(xmlDoc, 'pp_subtitle') || '';
|
|
1963
1975
|
const language = findPropertyValue(xmlDoc, 'pp_lang') || 'es';
|
|
@@ -1969,6 +1981,7 @@ function parseProject(entries) {
|
|
|
1969
1981
|
title,
|
|
1970
1982
|
subtitle,
|
|
1971
1983
|
language,
|
|
1984
|
+
exeVersion,
|
|
1972
1985
|
pages: sortPagesHierarchically(pages),
|
|
1973
1986
|
};
|
|
1974
1987
|
}
|
|
@@ -4953,6 +4966,17 @@ function toDocxHeadingLevel(level) {
|
|
|
4953
4966
|
return HeadingLevel.HEADING_6;
|
|
4954
4967
|
}
|
|
4955
4968
|
}
|
|
4969
|
+
// eXeLearning records the release that wrote the project as an odeResource.
|
|
4970
|
+
// It is descriptive only -- nothing in the runtime reads it to decide anything.
|
|
4971
|
+
function findResourceValue(xmlDoc, key) {
|
|
4972
|
+
const nodes = Array.from(xmlDoc.getElementsByTagName('odeResource'));
|
|
4973
|
+
for (const node of nodes) {
|
|
4974
|
+
if (getDirectText(node, 'key') === key) {
|
|
4975
|
+
return getDirectText(node, 'value');
|
|
4976
|
+
}
|
|
4977
|
+
}
|
|
4978
|
+
return null;
|
|
4979
|
+
}
|
|
4956
4980
|
function findPropertyValue(xmlDoc, key) {
|
|
4957
4981
|
const nodes = Array.from(xmlDoc.getElementsByTagName('odeProperty'));
|
|
4958
4982
|
for (const node of nodes) {
|
|
@@ -2,6 +2,7 @@ import { unzipSync, zipSync } from 'fflate';
|
|
|
2
2
|
import mammoth from 'mammoth';
|
|
3
3
|
import { MathMLToLaTeX } from 'mathml-to-latex';
|
|
4
4
|
import { DOMParser as XmldomParser, XMLSerializer as XmldomSerializer } from '@xmldom/xmldom';
|
|
5
|
+
import { EXE_RUNTIME_VERSION } from './exe-runtime.js';
|
|
5
6
|
// @ts-expect-error the vendored omml2mathml ships no TypeScript declarations.
|
|
6
7
|
import omml2mathml from './vendor/omml2mathml/index.js';
|
|
7
8
|
import temml from 'temml';
|
|
@@ -636,7 +637,7 @@ function generateContentXml(project) {
|
|
|
636
637
|
<odeResources>
|
|
637
638
|
<odeResource><key>odeId</key><value>${escapeXml(odeId)}</value></odeResource>
|
|
638
639
|
<odeResource><key>odeVersionId</key><value>${escapeXml(odeVersionId)}</value></odeResource>
|
|
639
|
-
<odeResource><key>exe_version</key><value
|
|
640
|
+
<odeResource><key>exe_version</key><value>${EXE_RUNTIME_VERSION}</value></odeResource>
|
|
640
641
|
</odeResources>
|
|
641
642
|
<odeProperties>
|
|
642
643
|
<odeProperty><key>pp_title</key><value>${escapeXml(project.title || 'Documento importado')}</value></odeProperty>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// The eXeLearning release whose runtime is vendored in this repository, kept in
|
|
2
|
+
// step with app/public/exelearning/runtime-source.json by
|
|
3
|
+
// tests/runtime-version.test.mjs, which npm run sync:exe updates.
|
|
4
|
+
export const EXE_RUNTIME_VERSION = '4.0.3';
|
|
5
|
+
/**
|
|
6
|
+
* Compares two eXeLearning versions. Returns true when `version` is newer than
|
|
7
|
+
* the vendored runtime, which is the only case worth warning about: a project
|
|
8
|
+
* from an older release converts fine, one from a newer release may carry
|
|
9
|
+
* elements this code does not know about and would drop without saying so.
|
|
10
|
+
*/
|
|
11
|
+
export function isNewerThanRuntime(version) {
|
|
12
|
+
if (!version) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
const parse = (value) => value.trim().replace(/^v/, '').split('.').map(part => Number.parseInt(part, 10));
|
|
16
|
+
const candidate = parse(version);
|
|
17
|
+
const runtime = parse(EXE_RUNTIME_VERSION);
|
|
18
|
+
if (candidate.some(Number.isNaN)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
for (let index = 0; index < Math.max(candidate.length, runtime.length); index += 1) {
|
|
22
|
+
const left = candidate[index] ?? 0;
|
|
23
|
+
const right = runtime[index] ?? 0;
|
|
24
|
+
if (left !== right) {
|
|
25
|
+
return left > right;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return false;
|
|
29
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "execonvert",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,7 +40,8 @@
|
|
|
40
40
|
"test:runtime": "node scripts/test-runtime.mjs",
|
|
41
41
|
"test:cli": "node --test tests/cli-update.test.mjs",
|
|
42
42
|
"test:docx": "node --test tests/docx-omml.test.mjs",
|
|
43
|
-
"test:args": "node --test tests/cli-args.test.mjs"
|
|
43
|
+
"test:args": "node --test tests/cli-args.test.mjs",
|
|
44
|
+
"test:version": "node --import tsx --test tests/runtime-version.test.mjs"
|
|
44
45
|
},
|
|
45
46
|
"dependencies": {
|
|
46
47
|
"@resvg/resvg-js": "^2.6.2",
|