execonvert 0.5.4 → 0.5.6
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 +31 -0
- package/dist/cli/cli/execonvert.js +31 -5
- package/dist/cli/src/converter.js +30 -6
- package/dist/cli/src/docx-import.js +11 -1
- package/dist/cli/src/exe-runtime.js +29 -0
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,37 @@ Categorías usadas: **Añadido**, **Cambiado**, **Corregido**, **Eliminado**.
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.5.6] - 2026-09-05
|
|
13
|
+
|
|
14
|
+
### Corregido
|
|
15
|
+
- La prima de las derivadas se anidaba un nivel en cada conversión de ida y
|
|
16
|
+
vuelta: `f^{'}` se convertía en `f^{^{'}}`, y repitiendo el ciclo seguía
|
|
17
|
+
acumulando capas. La causa es que temml escribe la prima como un superíndice
|
|
18
|
+
sobre una base vacía, que al volver de MathML se traducía literalmente. Solo
|
|
19
|
+
se colapsa esa forma: un superíndice doble real, que sí tiene base, no se
|
|
20
|
+
toca.
|
|
21
|
+
- Las dependencias del repositorio estaban ancladas a versiones anteriores a
|
|
22
|
+
las que instalaba cualquier usuario del paquete (`mathml-to-latex` 1.5.0
|
|
23
|
+
frente a 1.8.0), de modo que las pruebas no ejercitaban el mismo código que
|
|
24
|
+
se ejecuta al usarlo. Con ambas al día, un proyecto con 22 fórmulas
|
|
25
|
+
sobrevive tres ciclos `.elpx → .docx → .elpx` sin una sola diferencia.
|
|
26
|
+
|
|
27
|
+
## [0.5.5] - 2026-09-05
|
|
28
|
+
|
|
29
|
+
### Añadido
|
|
30
|
+
- `execonvert inspect` muestra la versión de eXeLearning con la que se creó el
|
|
31
|
+
proyecto, y la incluye en su salida `--json` como `exeVersion`.
|
|
32
|
+
- Aviso al convertir un `.elpx` creado con una versión de eXeLearning más
|
|
33
|
+
reciente que la que incorpora eXeConvert: la conversión sigue adelante, pero
|
|
34
|
+
puede no reconocer los elementos nuevos. Los proyectos de versiones iguales o
|
|
35
|
+
anteriores no muestran nada, porque funcionan.
|
|
36
|
+
|
|
37
|
+
### Corregido
|
|
38
|
+
- Los `.elpx` generados declaraban `exe_version` 3.0, un valor heredado de la
|
|
39
|
+
plantilla, cuando el runtime incorporado es el 4.0.3. Ahora declaran la
|
|
40
|
+
versión real, que es la que escribe eXeLearning en sus propios archivos. El
|
|
41
|
+
campo es informativo: nada en el runtime lo usa para tomar decisiones.
|
|
42
|
+
|
|
12
43
|
## [0.5.4] - 2026-09-05
|
|
13
44
|
|
|
14
45
|
### 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.6'.startsWith('__')
|
|
14
15
|
? String(JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version)
|
|
15
|
-
: '0.5.
|
|
16
|
+
: '0.5.6';
|
|
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>
|
|
@@ -1015,6 +1016,10 @@ function stripHtmlFromMath(value) {
|
|
|
1015
1016
|
const document = new DOMParser().parseFromString(`<!doctype html><html><body>${withLineBreaks}</body></html>`, 'text/html');
|
|
1016
1017
|
return document.body.textContent || '';
|
|
1017
1018
|
}
|
|
1019
|
+
/** Exposed for tests: the LaTeX normalisation applied to every converted formula. */
|
|
1020
|
+
export function normalizeLatexForTests(value) {
|
|
1021
|
+
return normalizeLatexValue(value);
|
|
1022
|
+
}
|
|
1018
1023
|
function normalizeLatexValue(value) {
|
|
1019
1024
|
let output = value.normalize('NFC').replace(/\u00a0/g, ' ').replace(/\r/g, '');
|
|
1020
1025
|
output = output.replace(/[\uFFFD\uFEFF\u00AD\u2066-\u2069\u200B-\u200F\u202A-\u202E\uFFF9-\uFFFB]/g, '');
|
|
@@ -1027,6 +1032,11 @@ function normalizeLatexValue(value) {
|
|
|
1027
1032
|
output = output.replace(/(^|[^\\])\.{2}\s+\\\\/g, '$1\\ldots ');
|
|
1028
1033
|
output = output.replace(/\\\\backslash\b/g, '\\\\');
|
|
1029
1034
|
output = output.replace(/\\backslash\b/g, '\\\\');
|
|
1035
|
+
// temml writes a prime as a superscript with an empty base, so a round trip
|
|
1036
|
+
// through MathML turns f^{'} into f^{^{'}} and nests one level deeper every
|
|
1037
|
+
// time. Only the empty-base form is collapsed: a real double superscript
|
|
1038
|
+
// carries a base and is left alone.
|
|
1039
|
+
output = output.replace(/\^\{\s*\^\{\s*([\u2032\u2033\u2034'`]+)\s*\}\s*\}/g, "^{$1}");
|
|
1030
1040
|
output = output.replace(/[ \t]+/g, ' ');
|
|
1031
1041
|
output = output.replace(/ ?\n ?/g, '\n');
|
|
1032
1042
|
output = output.trim();
|
|
@@ -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.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -40,7 +40,9 @@
|
|
|
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",
|
|
45
|
+
"test:primes": "node --import tsx --test tests/latex-primes.test.mjs"
|
|
44
46
|
},
|
|
45
47
|
"dependencies": {
|
|
46
48
|
"@resvg/resvg-js": "^2.6.2",
|