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 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.4'.startsWith('__')
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.4';
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 pages = buildPageRefs(await inspectElpxPages(inputFile));
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 project.pages.map(page => ({
1837
- id: page.id,
1838
- parentId: page.parentId,
1839
- title: page.title,
1840
- depth: page.depth,
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>3.0</value></odeResource>
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.4",
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",