execonvert 0.5.1 → 0.5.3

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,28 @@ Categorías usadas: **Añadido**, **Cambiado**, **Corregido**, **Eliminado**.
9
9
 
10
10
  ## [Unreleased]
11
11
 
12
+ ## [0.5.3] - 2026-09-05
13
+
14
+ ### Corregido
15
+ - Los `.elpx` generados desde `.docx` y `.md` se escribían sin comprimir, de
16
+ modo que ocupaban unas cuatro veces más de lo debido: un proyecto que
17
+ eXeLearning guarda en 2,4 MB salía de 10,3 MB. El empaquetado usa ahora el
18
+ mismo nivel de compresión habitual del formato, sin coste apreciable de
19
+ tiempo. Los archivos anteriores siguen abriéndose con normalidad.
20
+
21
+ ## [0.5.2] - 2026-09-05
22
+
23
+ ### Corregido
24
+ - Las fórmulas de los `.docx` se perdían al convertir desde la línea de órdenes:
25
+ el documento se recorría buscando los nodos `m:oMath` por espacio de nombres,
26
+ algo que el navegador resuelve pero linkedom —el DOM que usa la CLI— no, de
27
+ modo que no encontraba ninguna y las fórmulas desaparecían sin aviso. Ahora la
28
+ búsqueda funciona en ambos entornos y el fragmento OMML se reinterpreta con
29
+ `@xmldom/xmldom` antes de convertirlo, que es lo que el conversor entiende.
30
+ La versión web no estaba afectada. Se añade una prueba de regresión
31
+ (`npm run test:docx`) que comprueba que una fórmula llega a `content.xml`
32
+ convertida en LaTeX.
33
+
12
34
  ## [0.5.1] - 2026-09-05
13
35
 
14
36
  ### Cambiado
@@ -10,9 +10,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
12
  import { automaticCheck, autoCheckAllowed, runUpdate, updateText } from './updates.js';
13
- const CLI_VERSION = '0.5.1'.startsWith('__')
13
+ const CLI_VERSION = '0.5.3'.startsWith('__')
14
14
  ? String(JSON.parse(await readFile(new URL('../package.json', import.meta.url), 'utf8')).version)
15
- : '0.5.1';
15
+ : '0.5.3';
16
16
  const cliMessages = {
17
17
  es: {
18
18
  'help.title': 'CLI de eXeConvert',
@@ -1,7 +1,7 @@
1
1
  import { unzipSync, zipSync } from 'fflate';
2
2
  import mammoth from 'mammoth';
3
3
  import { MathMLToLaTeX } from 'mathml-to-latex';
4
- import { XMLSerializer as XmldomSerializer } from '@xmldom/xmldom';
4
+ import { DOMParser as XmldomParser, XMLSerializer as XmldomSerializer } from '@xmldom/xmldom';
5
5
  // @ts-expect-error the vendored omml2mathml ships no TypeScript declarations.
6
6
  import omml2mathml from './vendor/omml2mathml/index.js';
7
7
  import temml from 'temml';
@@ -197,16 +197,16 @@ async function preprocessDocxMath(inputBuffer) {
197
197
  const document = parseXml(documentXml, 'No se ha podido interpretar word/document.xml.');
198
198
  const formulas = new Map();
199
199
  let formulaIndex = 1;
200
- const blockMathNodes = Array.from(document.getElementsByTagNameNS(M_NS, 'oMathPara'));
200
+ const blockMathNodes = findOmmlNodes(document, 'oMathPara');
201
201
  for (const mathNode of blockMathNodes) {
202
202
  const placeholder = createMathPlaceholder(formulaIndex++);
203
203
  const latex = await convertOmmlElementToLatex(mathNode);
204
204
  formulas.set(placeholder, `\\[${latex}\\]`);
205
205
  replaceMathNodeWithPlaceholder(document, mathNode, placeholder);
206
206
  }
207
- const inlineMathNodes = Array.from(document.getElementsByTagNameNS(M_NS, 'oMath'));
207
+ const inlineMathNodes = findOmmlNodes(document, 'oMath');
208
208
  for (const mathNode of inlineMathNodes) {
209
- if (mathNode.parentElement?.namespaceURI === M_NS && mathNode.parentElement.localName === 'oMathPara') {
209
+ if (isOmmlElement(mathNode.parentElement, 'oMathPara')) {
210
210
  continue;
211
211
  }
212
212
  const placeholder = createMathPlaceholder(formulaIndex++);
@@ -221,9 +221,57 @@ async function preprocessDocxMath(inputBuffer) {
221
221
  const patchedBuffer = patchedBytes.buffer.slice(patchedBytes.byteOffset, patchedBytes.byteOffset + patchedBytes.byteLength);
222
222
  return { arrayBuffer: patchedBuffer, formulas };
223
223
  }
224
+ // Word writes formulas as <m:oMath>. Looking them up by namespace is the correct
225
+ // way and works in the browser, but linkedom -- the DOM the CLI runs on -- does
226
+ // not resolve namespaces in XML documents and finds nothing, which silently
227
+ // dropped every formula from .docx files converted from the command line.
228
+ // So: namespace first, qualified name as a fallback.
229
+ function findOmmlNodes(document, localName) {
230
+ const byNamespace = Array.from(document.getElementsByTagNameNS(M_NS, localName));
231
+ if (byNamespace.length > 0) {
232
+ return byNamespace;
233
+ }
234
+ // Not even getElementsByTagName('*') is usable there: on an XML document it
235
+ // returns nothing. Walking the tree by hand is what survives both DOMs.
236
+ const found = [];
237
+ const visit = (node) => {
238
+ for (const child of Array.from(node.childNodes)) {
239
+ if (child.nodeType !== 1) {
240
+ continue;
241
+ }
242
+ const element = child;
243
+ if (isOmmlElement(element, localName)) {
244
+ found.push(element);
245
+ }
246
+ visit(element);
247
+ }
248
+ };
249
+ visit(document);
250
+ return found;
251
+ }
252
+ function isOmmlElement(element, localName) {
253
+ if (!element) {
254
+ return false;
255
+ }
256
+ // linkedom keeps the prefix inside localName and reports the wrong namespace,
257
+ // so compare the name with any prefix stripped, in both DOMs.
258
+ const name = (element.localName ?? element.tagName ?? '').replace(/^[^:]*:/, '');
259
+ if (name !== localName) {
260
+ return false;
261
+ }
262
+ return element.namespaceURI === M_NS || (element.tagName ?? '').startsWith('m:');
263
+ }
224
264
  async function convertOmmlElementToLatex(element) {
225
265
  try {
226
- const mathElement = omml2mathml(element);
266
+ // omml2mathml returns null when fed linkedom nodes, so the fragment is
267
+ // reparsed with the same DOM it builds its output on. Serializing it detaches
268
+ // it from the namespace declarations on <w:document>, so they are restated on
269
+ // a wrapper; without them the fragment is not well-formed XML.
270
+ const ommlSource = new XMLSerializer().serializeToString(element);
271
+ const wrapped = `<w:wrapper xmlns:w="${W_NS}" xmlns:m="${M_NS}">${ommlSource}</w:wrapper>`;
272
+ const ommlDocument = new XmldomParser().parseFromString(wrapped, 'text/xml');
273
+ const ommlRoot = ommlDocument.documentElement?.firstChild;
274
+ const mathElement = omml2mathml(ommlRoot);
227
275
  // El árbol MathML lo construye @xmldom/xmldom, así que lo serializa el suyo:
228
276
  // el XMLSerializer global es el del entorno (linkedom en la CLI) y no
229
277
  // entiende esos nodos.
@@ -392,7 +440,10 @@ function buildElpxFromTemplate(template, project) {
392
440
  const { entries } = template;
393
441
  entries['content.xml'] = new TextEncoder().encode(generateContentXml(project));
394
442
  addPreviewHtmlEntries(entries, project);
395
- return zipSync(entries, { level: 0 });
443
+ // Stored entries made a .elpx four times bigger than the one eXeLearning
444
+ // writes for the same project. Level 6 is what the format is usually packed
445
+ // with, and the bundled runtime compresses well.
446
+ return zipSync(entries, { level: 6 });
396
447
  }
397
448
  function buildStandalonePreviewPages(project, entries, previewOptions = {}) {
398
449
  const pages = getPreviewPages(project);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "execonvert",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -38,7 +38,8 @@
38
38
  "prepack": "npm run build:cli",
39
39
  "test:updates": "node --import tsx --test tests/updates.test.ts",
40
40
  "test:runtime": "node scripts/test-runtime.mjs",
41
- "test:cli": "node --test tests/cli-update.test.mjs"
41
+ "test:cli": "node --test tests/cli-update.test.mjs",
42
+ "test:docx": "node --test tests/docx-omml.test.mjs"
42
43
  },
43
44
  "dependencies": {
44
45
  "@resvg/resvg-js": "^2.6.2",