execonvert 0.4.0
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 +79 -0
- package/README.md +194 -0
- package/THIRD_PARTY_NOTICES.md +42 -0
- package/bin/execonvert.js +21 -0
- package/dist/cli/app/public/THIRD_PARTY_NOTICES.md +32 -0
- package/dist/cli/app/public/base.elpx +0 -0
- package/dist/cli/app/public/exe_math_assets.zip +0 -0
- package/dist/cli/app/public/exelearning/app/common/common_i18n.js +79 -0
- package/dist/cli/app/public/exelearning/app/common/exe_powered_logo/exe_powered_logo.png +0 -0
- package/dist/cli/app/public/exelearning/app/common/i18n/common_i18n.ca.js +79 -0
- package/dist/cli/app/public/exelearning/app/common/i18n/common_i18n.de.js +79 -0
- package/dist/cli/app/public/exelearning/app/common/i18n/common_i18n.en.js +79 -0
- package/dist/cli/app/public/exelearning/app/common/i18n/common_i18n.eo.js +79 -0
- package/dist/cli/app/public/exelearning/app/common/i18n/common_i18n.es.js +79 -0
- package/dist/cli/app/public/exelearning/app/common/i18n/common_i18n.eu.js +79 -0
- package/dist/cli/app/public/exelearning/app/common/i18n/common_i18n.gl.js +79 -0
- package/dist/cli/app/public/exelearning/app/common/i18n/common_i18n.it.js +79 -0
- package/dist/cli/app/public/exelearning/app/common/i18n/common_i18n.pt.js +79 -0
- package/dist/cli/app/public/exelearning/app/common/i18n/common_i18n.ro.js +79 -0
- package/dist/cli/app/public/exelearning/app/common/i18n/common_i18n.va.js +79 -0
- package/dist/cli/app/public/exelearning/bundles/common.zip +0 -0
- package/dist/cli/app/public/exelearning/bundles/content-css.zip +0 -0
- package/dist/cli/app/public/exelearning/bundles/idevices.zip +0 -0
- package/dist/cli/app/public/exelearning/bundles/libs.zip +0 -0
- package/dist/cli/app/public/exelearning/bundles/manifest.json +110 -0
- package/dist/cli/app/public/exelearning/bundles/themes/base.zip +0 -0
- package/dist/cli/app/public/exelearning/bundles/themes/flux.zip +0 -0
- package/dist/cli/app/public/exelearning/bundles/themes/neo.zip +0 -0
- package/dist/cli/app/public/exelearning/bundles/themes/nova.zip +0 -0
- package/dist/cli/app/public/exelearning/bundles/themes/universal.zip +0 -0
- package/dist/cli/app/public/exelearning/bundles/themes/zen.zip +0 -0
- package/dist/cli/app/public/exelearning/exporters.bundle.js +12638 -0
- package/dist/cli/app/public/exelearning/importers.bundle.js +14061 -0
- package/dist/cli/app/public/exelearning/runtime-source.json +10 -0
- package/dist/cli/app/public/favicon.svg +11 -0
- package/dist/cli/app/public/idevices/text/text.css +16 -0
- package/dist/cli/app/public/idevices/text/text.html +3 -0
- package/dist/cli/app/public/idevices/text/text.js +291 -0
- package/dist/cli/app/public/info/cli.html +955 -0
- package/dist/cli/app/public/info/docx.svg +7 -0
- package/dist/cli/app/public/info/elpx-icon.png +0 -0
- package/dist/cli/app/public/info/exe-old.png +0 -0
- package/dist/cli/app/public/info/execonvert-cli-icon.svg +16 -0
- package/dist/cli/app/public/info/execonvert-icon.svg +11 -0
- package/dist/cli/app/public/info/index.html +723 -0
- package/dist/cli/app/public/info/markdown.svg +1 -0
- package/dist/cli/app/public/info/pdf.svg +14 -0
- package/dist/cli/app/public/libs/exe_math/LICENSE +202 -0
- package/dist/cli/app/public/libs/exe_math/a11y/assistive-mml.js +1 -0
- package/dist/cli/app/public/libs/exe_math/a11y/complexity.js +1 -0
- package/dist/cli/app/public/libs/exe_math/a11y/explorer.js +1 -0
- package/dist/cli/app/public/libs/exe_math/a11y/semantic-enrich.js +1 -0
- package/dist/cli/app/public/libs/exe_math/a11y/speech.js +1 -0
- package/dist/cli/app/public/libs/exe_math/a11y/sre.js +1 -0
- package/dist/cli/app/public/libs/exe_math/adaptors/jsdom.js +1 -0
- package/dist/cli/app/public/libs/exe_math/adaptors/linkedom.js +1 -0
- package/dist/cli/app/public/libs/exe_math/adaptors/liteDOM.js +1 -0
- package/dist/cli/app/public/libs/exe_math/core.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/asciimath.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/mml/entities.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/mml/extensions/mml3.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/mml/extensions/mml3.sef.json +1 -0
- package/dist/cli/app/public/libs/exe_math/input/mml.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/action.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/ams.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/amscd.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/autoload.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/bbm.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/bboldx.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/bbox.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/begingroup.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/boldsymbol.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/braket.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/bussproofs.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/cancel.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/cases.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/centernot.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/color.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/colortbl.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/colorv2.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/configmacros.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/dsfont.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/empheq.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/enclose.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/extpfeil.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/gensymb.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/html.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/mathtools.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/mhchem.js +34 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/newcommand.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/noerrors.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/noundefined.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/physics.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/require.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/setoptions.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/tagformat.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/texhtml.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/textcomp.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/textmacros.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/unicode.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/units.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/upgreek.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex/extensions/verb.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex-base.js +1 -0
- package/dist/cli/app/public/libs/exe_math/input/tex.js +1 -0
- package/dist/cli/app/public/libs/exe_math/loader.js +1 -0
- package/dist/cli/app/public/libs/exe_math/output/chtml.js +1 -0
- package/dist/cli/app/public/libs/exe_math/output/svg.js +1 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/af.json +146 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/base.json +137 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/ca.json +140 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/da.json +140 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/de.json +146 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/en.json +158 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/es.json +140 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/euro.json +32 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/fr.json +146 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/hi.json +146 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/it.json +146 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/ko.json +146 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/nb.json +146 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/nemeth.json +125 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/nn.json +146 -0
- package/dist/cli/app/public/libs/exe_math/sre/mathmaps/sv.json +146 -0
- package/dist/cli/app/public/libs/exe_math/sre/require.d.mts +1 -0
- package/dist/cli/app/public/libs/exe_math/sre/require.mjs +2 -0
- package/dist/cli/app/public/libs/exe_math/sre/speech-worker.js +1 -0
- package/dist/cli/app/public/libs/exe_math/startup.js +1 -0
- package/dist/cli/app/public/libs/exe_math/tex-mml-svg.js +1 -0
- package/dist/cli/app/public/libs/exe_math/ui/lazy.js +1 -0
- package/dist/cli/app/public/libs/exe_math/ui/menu.js +1 -0
- package/dist/cli/app/public/libs/exe_math/ui/safe.js +1 -0
- package/dist/cli/cli/execonvert.js +807 -0
- package/dist/cli/cli/runtime.js +237 -0
- package/dist/cli/package.json +3 -0
- package/dist/cli/src/converter.js +5078 -0
- package/dist/cli/src/docx-import.js +1121 -0
- package/dist/cli/src/elpx-markdown.js +201 -0
- package/dist/cli/src/i18n.js +485 -0
- package/dist/cli/src/index.js +1 -0
- package/dist/cli/src/legacy-elp.js +846 -0
- package/dist/cli/src/markdown-import.js +26 -0
- package/package.json +68 -0
|
@@ -0,0 +1,1121 @@
|
|
|
1
|
+
import { unzipSync, zipSync } from 'fflate';
|
|
2
|
+
import mammoth from 'mammoth';
|
|
3
|
+
import { MathMLToLaTeX } from 'mathml-to-latex';
|
|
4
|
+
import temml from 'temml';
|
|
5
|
+
const EXECONVERT_SIGNATURE = 'eXeConvert v0.2.2';
|
|
6
|
+
let cachedOmml2mathml = null;
|
|
7
|
+
export async function convertDocxToElpx(file, options, onProgress) {
|
|
8
|
+
onProgress?.({ phase: 'read', message: 'Leyendo el archivo .docx...', messageKey: 'progress.readDocx' });
|
|
9
|
+
const inputBuffer = await file.arrayBuffer();
|
|
10
|
+
onProgress?.({ phase: 'parse', message: 'Analizando estilos y contenido del DOCX...', messageKey: 'progress.parseDocxStyles' });
|
|
11
|
+
const htmlValue = await extractDocxHtml(inputBuffer);
|
|
12
|
+
return convertHtmlToElpx(htmlValue, file.name, options, onProgress, 'progress.parseDocumentStructure', { renderLatex: true });
|
|
13
|
+
}
|
|
14
|
+
export async function convertHtmlToElpx(htmlValue, filename, options, onProgress, parseMessageKey = 'progress.parseDocumentStructure', previewOptions = {}) {
|
|
15
|
+
onProgress?.({ phase: 'parse', message: 'Interpretando la estructura del documento...', messageKey: parseMessageKey });
|
|
16
|
+
const project = buildProjectFromHtml(htmlValue, filename, options);
|
|
17
|
+
return convertProjectToElpx(project, filename, undefined, onProgress, previewOptions);
|
|
18
|
+
}
|
|
19
|
+
export async function convertProjectToElpx(project, filename, extraEntries, onProgress, previewOptions = {}) {
|
|
20
|
+
onProgress?.({ phase: 'template', message: 'Aplicando la plantilla base de eXeLearning...', messageKey: 'progress.applyTemplate' });
|
|
21
|
+
const template = await loadBaseTemplate();
|
|
22
|
+
if (extraEntries) {
|
|
23
|
+
for (const [entryPath, entryData] of Object.entries(extraEntries)) {
|
|
24
|
+
template.entries[entryPath] = entryData;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
const previewPages = buildStandalonePreviewPages(project, template.entries, previewOptions);
|
|
28
|
+
const previewHtml = previewPages['index.html']
|
|
29
|
+
? previewPages['index.html']
|
|
30
|
+
: '<!doctype html><html lang="es"><body><p>Sin contenido para previsualizar.</p></body></html>';
|
|
31
|
+
const elpxData = buildElpxFromTemplate(template, project);
|
|
32
|
+
onProgress?.({ phase: 'pack', message: 'Generando el archivo .elpx...', messageKey: 'progress.packElpx' });
|
|
33
|
+
const blobData = new Uint8Array(elpxData);
|
|
34
|
+
const blob = new Blob([blobData], { type: 'application/zip' });
|
|
35
|
+
return {
|
|
36
|
+
blob,
|
|
37
|
+
filename: toElpxFilename(filename),
|
|
38
|
+
pageCount: project.pages.length,
|
|
39
|
+
blockCount: project.pages.reduce((count, page) => count + page.blocks.length, 0),
|
|
40
|
+
previewHtml,
|
|
41
|
+
previewPages,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async function extractDocxHtml(inputBuffer) {
|
|
45
|
+
const { arrayBuffer: patchedBuffer, formulas } = await preprocessDocxMath(inputBuffer);
|
|
46
|
+
const mammothInput = typeof Buffer !== 'undefined'
|
|
47
|
+
? { buffer: Buffer.from(patchedBuffer) }
|
|
48
|
+
: { arrayBuffer: patchedBuffer };
|
|
49
|
+
const result = await mammoth.convertToHtml(mammothInput, {
|
|
50
|
+
includeEmbeddedStyleMap: true,
|
|
51
|
+
includeDefaultStyleMap: true,
|
|
52
|
+
ignoreEmptyParagraphs: true,
|
|
53
|
+
styleMap: DOCX_STYLE_MAP,
|
|
54
|
+
convertImage: mammoth.images.imgElement(async (image) => ({
|
|
55
|
+
src: `data:${image.contentType};base64,${await image.readAsBase64String()}`,
|
|
56
|
+
})),
|
|
57
|
+
});
|
|
58
|
+
let htmlValue = result.value;
|
|
59
|
+
const formulaEntries = Array.from(formulas.entries()).sort((a, b) => b[0].length - a[0].length);
|
|
60
|
+
for (const [placeholder, latex] of formulaEntries) {
|
|
61
|
+
htmlValue = htmlValue.replaceAll(placeholder, escapeHtml(latex));
|
|
62
|
+
}
|
|
63
|
+
return htmlValue;
|
|
64
|
+
}
|
|
65
|
+
function buildProjectFromHtml(htmlValue, filename, options) {
|
|
66
|
+
const document = new DOMParser().parseFromString(`<!doctype html><html><body>${htmlValue}</body></html>`, 'text/html');
|
|
67
|
+
const body = document.body;
|
|
68
|
+
let resourceTitle = '';
|
|
69
|
+
let resourceTitleAssigned = false;
|
|
70
|
+
const pages = [];
|
|
71
|
+
let currentPage = null;
|
|
72
|
+
let currentBlock = null;
|
|
73
|
+
let currentTopLevelPage = null;
|
|
74
|
+
let currentSecondLevelPage = null;
|
|
75
|
+
let currentThirdLevelPage = null;
|
|
76
|
+
for (const node of Array.from(body.childNodes)) {
|
|
77
|
+
if (!(node instanceof HTMLElement)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const tag = node.tagName.toLowerCase();
|
|
81
|
+
const cleanedHtml = normalizeMathMarkup(sanitizeImportedHtml(node.outerHTML));
|
|
82
|
+
const trimmed = normalizeWhitespace(node.textContent || '');
|
|
83
|
+
if (!trimmed && !hasMeaningfulHtml(cleanedHtml)) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const headingMatch = /^h([1-6])$/.exec(tag);
|
|
87
|
+
if (headingMatch) {
|
|
88
|
+
const rawLevel = Number(headingMatch[1]);
|
|
89
|
+
const useResourceTitle = options.heading1Mode === 'resource';
|
|
90
|
+
if (useResourceTitle && rawLevel === 1 && !resourceTitleAssigned) {
|
|
91
|
+
resourceTitle = trimmed;
|
|
92
|
+
resourceTitleAssigned = true;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const effectiveLevel = useResourceTitle && rawLevel > 1 ? rawLevel - 1 : rawLevel;
|
|
96
|
+
if (effectiveLevel < 1 || effectiveLevel > 4) {
|
|
97
|
+
currentPage = ensurePage(pages, currentPage);
|
|
98
|
+
currentBlock = ensureBlock(currentPage, currentBlock);
|
|
99
|
+
currentBlock.html = appendParagraphHtml(currentBlock.html, cleanedHtml);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (effectiveLevel === 1) {
|
|
103
|
+
currentPage = createPage(pages, trimmed, 1, null);
|
|
104
|
+
currentTopLevelPage = currentPage;
|
|
105
|
+
currentSecondLevelPage = null;
|
|
106
|
+
currentThirdLevelPage = null;
|
|
107
|
+
currentBlock = null;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (effectiveLevel === 2) {
|
|
111
|
+
if (options.heading2Mode === 'page') {
|
|
112
|
+
const parentPage = currentTopLevelPage ?? ensurePage(pages, currentPage);
|
|
113
|
+
currentPage = createPage(pages, trimmed, 2, pages.indexOf(parentPage));
|
|
114
|
+
currentSecondLevelPage = currentPage;
|
|
115
|
+
currentThirdLevelPage = null;
|
|
116
|
+
currentBlock = null;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
currentPage = ensurePage(pages, currentPage);
|
|
120
|
+
currentBlock = { title: trimmed, html: '' };
|
|
121
|
+
currentPage.blocks.push(currentBlock);
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (effectiveLevel === 3) {
|
|
125
|
+
if (options.heading2Mode !== 'page') {
|
|
126
|
+
currentPage = ensurePage(pages, currentPage);
|
|
127
|
+
currentBlock = ensureBlock(currentPage, currentBlock);
|
|
128
|
+
currentBlock.html = appendParagraphHtml(currentBlock.html, cleanedHtml);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (options.heading3Mode === 'page') {
|
|
132
|
+
const parentPage = currentSecondLevelPage ?? currentTopLevelPage ?? ensurePage(pages, currentPage);
|
|
133
|
+
currentPage = createPage(pages, trimmed, 3, pages.indexOf(parentPage));
|
|
134
|
+
currentThirdLevelPage = currentPage;
|
|
135
|
+
currentBlock = null;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
currentPage = ensurePage(pages, currentPage);
|
|
139
|
+
currentBlock = { title: trimmed, html: '' };
|
|
140
|
+
currentPage.blocks.push(currentBlock);
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (options.heading2Mode !== 'page' || options.heading3Mode !== 'page') {
|
|
144
|
+
currentPage = ensurePage(pages, currentPage);
|
|
145
|
+
currentBlock = ensureBlock(currentPage, currentBlock);
|
|
146
|
+
currentBlock.html = appendParagraphHtml(currentBlock.html, cleanedHtml);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (options.heading4Mode === 'page') {
|
|
150
|
+
const parentPage = currentThirdLevelPage ?? currentSecondLevelPage ?? currentTopLevelPage ?? ensurePage(pages, currentPage);
|
|
151
|
+
currentPage = createPage(pages, trimmed, 4, pages.indexOf(parentPage));
|
|
152
|
+
currentBlock = null;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
currentPage = ensurePage(pages, currentPage);
|
|
156
|
+
currentBlock = { title: trimmed, html: '' };
|
|
157
|
+
currentPage.blocks.push(currentBlock);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
currentPage = ensurePage(pages, currentPage);
|
|
161
|
+
currentBlock = ensureBlock(currentPage, currentBlock);
|
|
162
|
+
currentBlock.html = appendParagraphHtml(currentBlock.html, cleanedHtml);
|
|
163
|
+
}
|
|
164
|
+
if (pages.length === 0) {
|
|
165
|
+
pages.push({
|
|
166
|
+
title: stemFromFilename(filename) || 'Página 1',
|
|
167
|
+
level: 1,
|
|
168
|
+
parentIndex: null,
|
|
169
|
+
blocks: [{ title: 'Contenido', html: '<p>Documento importado sin encabezados detectados.</p>' }],
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
for (const page of pages) {
|
|
173
|
+
if (page.blocks.length === 0) {
|
|
174
|
+
page.blocks.push({ title: 'Contenido', html: '<p></p>' });
|
|
175
|
+
continue;
|
|
176
|
+
}
|
|
177
|
+
for (const block of page.blocks) {
|
|
178
|
+
if (!block.html) {
|
|
179
|
+
block.html = '<p></p>';
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
title: resourceTitle || stemFromFilename(filename) || 'Documento importado',
|
|
185
|
+
subtitle: '',
|
|
186
|
+
pages,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
async function preprocessDocxMath(inputBuffer) {
|
|
190
|
+
const entries = unzipSync(new Uint8Array(inputBuffer));
|
|
191
|
+
const documentXml = decodeUtf8(entries['word/document.xml']);
|
|
192
|
+
if (!documentXml) {
|
|
193
|
+
return { arrayBuffer: inputBuffer, formulas: new Map() };
|
|
194
|
+
}
|
|
195
|
+
const document = parseXml(documentXml, 'No se ha podido interpretar word/document.xml.');
|
|
196
|
+
const formulas = new Map();
|
|
197
|
+
let formulaIndex = 1;
|
|
198
|
+
const blockMathNodes = Array.from(document.getElementsByTagNameNS(M_NS, 'oMathPara'));
|
|
199
|
+
for (const mathNode of blockMathNodes) {
|
|
200
|
+
const placeholder = createMathPlaceholder(formulaIndex++);
|
|
201
|
+
const latex = await convertOmmlElementToLatex(mathNode);
|
|
202
|
+
formulas.set(placeholder, `\\[${latex}\\]`);
|
|
203
|
+
replaceMathNodeWithPlaceholder(document, mathNode, placeholder);
|
|
204
|
+
}
|
|
205
|
+
const inlineMathNodes = Array.from(document.getElementsByTagNameNS(M_NS, 'oMath'));
|
|
206
|
+
for (const mathNode of inlineMathNodes) {
|
|
207
|
+
if (mathNode.parentElement?.namespaceURI === M_NS && mathNode.parentElement.localName === 'oMathPara') {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const placeholder = createMathPlaceholder(formulaIndex++);
|
|
211
|
+
const latex = await convertOmmlElementToLatex(mathNode);
|
|
212
|
+
formulas.set(placeholder, `\\(${latex}\\)`);
|
|
213
|
+
replaceMathNodeWithPlaceholder(document, mathNode, placeholder);
|
|
214
|
+
}
|
|
215
|
+
const serialized = new XMLSerializer().serializeToString(document);
|
|
216
|
+
entries['word/document.xml'] = new TextEncoder().encode(serialized);
|
|
217
|
+
const patched = zipSync(entries, { level: 0 });
|
|
218
|
+
const patchedBytes = new Uint8Array(patched);
|
|
219
|
+
const patchedBuffer = patchedBytes.buffer.slice(patchedBytes.byteOffset, patchedBytes.byteOffset + patchedBytes.byteLength);
|
|
220
|
+
return { arrayBuffer: patchedBuffer, formulas };
|
|
221
|
+
}
|
|
222
|
+
async function convertOmmlElementToLatex(element) {
|
|
223
|
+
try {
|
|
224
|
+
const mathElement = (await loadOmml2MathMl())(element);
|
|
225
|
+
const mathMl = new XMLSerializer().serializeToString(mathElement);
|
|
226
|
+
const latex = normalizeLatexValue(MathMLToLaTeX.convert(mathMl));
|
|
227
|
+
return latex || '?';
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return '?';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async function loadOmml2MathMl() {
|
|
234
|
+
if (cachedOmml2mathml) {
|
|
235
|
+
return cachedOmml2mathml;
|
|
236
|
+
}
|
|
237
|
+
const cliRequire = globalThis.__execonvertRequire;
|
|
238
|
+
if (typeof cliRequire === 'function') {
|
|
239
|
+
const loaded = cliRequire('omml2mathml');
|
|
240
|
+
cachedOmml2mathml = typeof loaded === 'function' ? loaded : loaded.default ?? null;
|
|
241
|
+
}
|
|
242
|
+
if (cachedOmml2mathml) {
|
|
243
|
+
return cachedOmml2mathml;
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
// @ts-expect-error omml2mathml does not ship TypeScript declarations.
|
|
247
|
+
const loaded = await import('omml2mathml');
|
|
248
|
+
cachedOmml2mathml = typeof loaded === 'function' ? loaded : loaded.default ?? null;
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (typeof process !== 'undefined' && typeof process.emitWarning === 'function') {
|
|
252
|
+
const originalEmitWarning = process.emitWarning.bind(process);
|
|
253
|
+
process.emitWarning = ((warning, ...args) => {
|
|
254
|
+
const warningCode = typeof args[0] === 'string' ? args[0] : undefined;
|
|
255
|
+
const warningMessage = typeof warning === 'string' ? warning : warning.message;
|
|
256
|
+
if (warningCode === 'DEP0040' && warningMessage.includes('`punycode` module is deprecated')) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
return originalEmitWarning(warning, ...args);
|
|
260
|
+
});
|
|
261
|
+
try {
|
|
262
|
+
// @ts-expect-error omml2mathml does not ship TypeScript declarations.
|
|
263
|
+
const loaded = await import('omml2mathml');
|
|
264
|
+
cachedOmml2mathml = typeof loaded === 'function' ? loaded : loaded.default ?? null;
|
|
265
|
+
}
|
|
266
|
+
finally {
|
|
267
|
+
process.emitWarning = originalEmitWarning;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
else {
|
|
271
|
+
throw error;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
if (!cachedOmml2mathml) {
|
|
275
|
+
throw new Error('No se ha podido cargar omml2mathml.');
|
|
276
|
+
}
|
|
277
|
+
return cachedOmml2mathml;
|
|
278
|
+
}
|
|
279
|
+
function replaceMathNodeWithPlaceholder(document, mathNode, placeholder) {
|
|
280
|
+
const run = document.createElementNS(W_NS, 'w:r');
|
|
281
|
+
const text = document.createElementNS(W_NS, 'w:t');
|
|
282
|
+
text.textContent = placeholder;
|
|
283
|
+
run.appendChild(text);
|
|
284
|
+
mathNode.replaceWith(run);
|
|
285
|
+
}
|
|
286
|
+
function createMathPlaceholder(index) {
|
|
287
|
+
return `__EXE_MATH_PLACEHOLDER_${index}__`;
|
|
288
|
+
}
|
|
289
|
+
function ensurePage(pages, currentPage) {
|
|
290
|
+
if (currentPage) {
|
|
291
|
+
return currentPage;
|
|
292
|
+
}
|
|
293
|
+
return createPage(pages, `Página ${pages.length + 1}`, 1, null);
|
|
294
|
+
}
|
|
295
|
+
function createPage(pages, title, level, parentIndex) {
|
|
296
|
+
const page = { title, level, parentIndex, blocks: [] };
|
|
297
|
+
pages.push(page);
|
|
298
|
+
return page;
|
|
299
|
+
}
|
|
300
|
+
function ensureBlock(page, currentBlock) {
|
|
301
|
+
if (currentBlock) {
|
|
302
|
+
return currentBlock;
|
|
303
|
+
}
|
|
304
|
+
const block = { title: 'Contenido', html: '' };
|
|
305
|
+
page.blocks.push(block);
|
|
306
|
+
return block;
|
|
307
|
+
}
|
|
308
|
+
function appendParagraphHtml(existing, paragraphHtml) {
|
|
309
|
+
return existing ? `${existing}\n${paragraphHtml}` : paragraphHtml;
|
|
310
|
+
}
|
|
311
|
+
function sanitizeImportedHtml(html) {
|
|
312
|
+
const document = new DOMParser().parseFromString(`<!doctype html><html><body>${html}</body></html>`, 'text/html');
|
|
313
|
+
return Array.from(document.body.childNodes)
|
|
314
|
+
.map(node => normalizeImportedNode(node))
|
|
315
|
+
.join('')
|
|
316
|
+
.trim();
|
|
317
|
+
}
|
|
318
|
+
function normalizeImportedNode(node) {
|
|
319
|
+
if (node.nodeType === Node.TEXT_NODE) {
|
|
320
|
+
return escapeHtml(node.textContent || '');
|
|
321
|
+
}
|
|
322
|
+
if (!(node instanceof HTMLElement)) {
|
|
323
|
+
return '';
|
|
324
|
+
}
|
|
325
|
+
const tag = node.tagName.toLowerCase();
|
|
326
|
+
const normalizedChildren = Array.from(node.childNodes)
|
|
327
|
+
.map(child => normalizeImportedNode(child))
|
|
328
|
+
.join('');
|
|
329
|
+
switch (tag) {
|
|
330
|
+
case 'div':
|
|
331
|
+
case 'span':
|
|
332
|
+
return normalizedChildren;
|
|
333
|
+
case 'b':
|
|
334
|
+
return wrapTag('strong', normalizedChildren);
|
|
335
|
+
case 'i':
|
|
336
|
+
return wrapTag('em', normalizedChildren);
|
|
337
|
+
case 'strike':
|
|
338
|
+
return wrapTag('del', normalizedChildren);
|
|
339
|
+
case 'p':
|
|
340
|
+
case 'ul':
|
|
341
|
+
case 'ol':
|
|
342
|
+
case 'li':
|
|
343
|
+
case 'table':
|
|
344
|
+
case 'thead':
|
|
345
|
+
case 'tbody':
|
|
346
|
+
case 'tr':
|
|
347
|
+
case 'th':
|
|
348
|
+
case 'td':
|
|
349
|
+
case 'strong':
|
|
350
|
+
case 'em':
|
|
351
|
+
case 'u':
|
|
352
|
+
case 'sup':
|
|
353
|
+
case 'sub':
|
|
354
|
+
case 'blockquote':
|
|
355
|
+
case 'pre':
|
|
356
|
+
case 'code':
|
|
357
|
+
case 'h3':
|
|
358
|
+
case 'h4':
|
|
359
|
+
case 'h5':
|
|
360
|
+
case 'h6':
|
|
361
|
+
return wrapTag(tag, normalizedChildren);
|
|
362
|
+
case 'br':
|
|
363
|
+
return '<br />';
|
|
364
|
+
case 'a': {
|
|
365
|
+
const href = (node.getAttribute('href') || '').trim();
|
|
366
|
+
if (!href) {
|
|
367
|
+
return normalizedChildren;
|
|
368
|
+
}
|
|
369
|
+
return `<a href="${escapeHtml(href)}">${normalizedChildren}</a>`;
|
|
370
|
+
}
|
|
371
|
+
case 'img': {
|
|
372
|
+
const src = (node.getAttribute('src') || '').trim();
|
|
373
|
+
if (!src) {
|
|
374
|
+
return '';
|
|
375
|
+
}
|
|
376
|
+
const alt = (node.getAttribute('alt') || '').trim();
|
|
377
|
+
const altAttribute = alt ? ` alt="${escapeHtml(alt)}"` : '';
|
|
378
|
+
return `<img src="${escapeHtml(src)}"${altAttribute} />`;
|
|
379
|
+
}
|
|
380
|
+
default:
|
|
381
|
+
return normalizedChildren;
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
function wrapTag(tag, innerHtml) {
|
|
385
|
+
if (!innerHtml && tag !== 'p' && tag !== 'td' && tag !== 'th') {
|
|
386
|
+
return '';
|
|
387
|
+
}
|
|
388
|
+
return `<${tag}>${innerHtml}</${tag}>`;
|
|
389
|
+
}
|
|
390
|
+
function hasMeaningfulHtml(html) {
|
|
391
|
+
if (!html) {
|
|
392
|
+
return false;
|
|
393
|
+
}
|
|
394
|
+
const text = normalizeWhitespace(html
|
|
395
|
+
.replace(/<br\s*\/?>/gi, ' ')
|
|
396
|
+
.replace(/<img\b[^>]*>/gi, ' [img] ')
|
|
397
|
+
.replace(/<table\b[\s\S]*?<\/table>/gi, ' [table] ')
|
|
398
|
+
.replace(/<[^>]+>/g, ' ')
|
|
399
|
+
.replace(/ /g, ' '));
|
|
400
|
+
return text.length > 0;
|
|
401
|
+
}
|
|
402
|
+
async function loadBaseTemplate() {
|
|
403
|
+
const baseUrl = import.meta.env?.BASE_URL ?? '/';
|
|
404
|
+
const response = await fetch(`${baseUrl}base.elpx`);
|
|
405
|
+
if (!response.ok) {
|
|
406
|
+
throw new Error('No se ha podido cargar la plantilla base integrada.');
|
|
407
|
+
}
|
|
408
|
+
const entries = unzipSync(new Uint8Array(await response.arrayBuffer()));
|
|
409
|
+
if (!entries['content.xml']) {
|
|
410
|
+
throw new Error('La plantilla base no contiene content.xml.');
|
|
411
|
+
}
|
|
412
|
+
await Promise.all(TEXT_IDEVICE_ASSETS.map(async (assetPath) => {
|
|
413
|
+
const assetResponse = await fetch(`${baseUrl}${assetPath}`);
|
|
414
|
+
if (!assetResponse.ok) {
|
|
415
|
+
throw new Error(`No se ha podido cargar el recurso ${assetPath}.`);
|
|
416
|
+
}
|
|
417
|
+
entries[assetPath] = new Uint8Array(await assetResponse.arrayBuffer());
|
|
418
|
+
}));
|
|
419
|
+
const mathAssetsResponse = await fetch(`${baseUrl}${EXE_MATH_ASSET_ARCHIVE}`);
|
|
420
|
+
if (!mathAssetsResponse.ok) {
|
|
421
|
+
throw new Error('No se ha podido cargar el paquete de MathJax integrado.');
|
|
422
|
+
}
|
|
423
|
+
const mathEntries = unzipSync(new Uint8Array(await mathAssetsResponse.arrayBuffer()));
|
|
424
|
+
for (const [assetPath, data] of Object.entries(mathEntries)) {
|
|
425
|
+
if (!assetPath.startsWith('exe_math/')) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
entries[`libs/${assetPath}`] = data;
|
|
429
|
+
}
|
|
430
|
+
return { entries };
|
|
431
|
+
}
|
|
432
|
+
function buildElpxFromTemplate(template, project) {
|
|
433
|
+
const { entries } = template;
|
|
434
|
+
entries['content.xml'] = new TextEncoder().encode(generateContentXml(project));
|
|
435
|
+
addPreviewHtmlEntries(entries, project);
|
|
436
|
+
return zipSync(entries, { level: 0 });
|
|
437
|
+
}
|
|
438
|
+
function buildStandalonePreviewPages(project, entries, previewOptions = {}) {
|
|
439
|
+
const pages = getPreviewPages(project);
|
|
440
|
+
const output = {};
|
|
441
|
+
for (const [index, pageInfo] of pages.entries()) {
|
|
442
|
+
const html = generatePreviewPageHtml(project, pages, index, previewOptions);
|
|
443
|
+
output[pageInfo.href] = buildStandalonePreviewHtml(html, entries, pageInfo.href);
|
|
444
|
+
}
|
|
445
|
+
return output;
|
|
446
|
+
}
|
|
447
|
+
function buildStandalonePreviewHtml(html, entries, docPath) {
|
|
448
|
+
const document = new DOMParser().parseFromString(html, 'text/html');
|
|
449
|
+
const docAny = document;
|
|
450
|
+
if (!docAny.querySelectorAll || !docAny.body) {
|
|
451
|
+
return html;
|
|
452
|
+
}
|
|
453
|
+
for (const link of Array.from(document.querySelectorAll('link[href]'))) {
|
|
454
|
+
const href = (link.getAttribute('href') || '').trim();
|
|
455
|
+
const resolved = resolveEntryPath(docPath, href, entries);
|
|
456
|
+
if (!resolved) {
|
|
457
|
+
link.remove();
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (/\.css$/i.test(resolved)) {
|
|
461
|
+
const style = document.createElement('style');
|
|
462
|
+
style.textContent = inlineCssAssetUrls(decodeUtf8(entries[resolved]), resolved, entries);
|
|
463
|
+
link.replaceWith(style);
|
|
464
|
+
continue;
|
|
465
|
+
}
|
|
466
|
+
link.remove();
|
|
467
|
+
}
|
|
468
|
+
for (const script of Array.from(document.querySelectorAll('script'))) {
|
|
469
|
+
script.remove();
|
|
470
|
+
}
|
|
471
|
+
const mediaNodes = Array.from(document.querySelectorAll('[src], [poster], source[src], track[src]'));
|
|
472
|
+
for (const node of mediaNodes) {
|
|
473
|
+
for (const attribute of ['src', 'poster']) {
|
|
474
|
+
const rawValue = (node.getAttribute(attribute) || '').trim();
|
|
475
|
+
if (!rawValue) {
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const resolved = resolveEntryPath(docPath, rawValue, entries);
|
|
479
|
+
if (!resolved) {
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
const mime = getMimeTypeFromPath(resolved);
|
|
483
|
+
const dataUrl = `data:${mime};base64,${uint8ToBase64(entries[resolved])}`;
|
|
484
|
+
node.setAttribute(attribute, dataUrl);
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
for (const frameLike of Array.from(document.querySelectorAll('iframe, object, embed'))) {
|
|
488
|
+
const replacement = document.createElement('div');
|
|
489
|
+
replacement.className = 'preview-embed-placeholder';
|
|
490
|
+
replacement.textContent = 'Contenido incrustado omitido en la vista previa.';
|
|
491
|
+
frameLike.replaceWith(replacement);
|
|
492
|
+
}
|
|
493
|
+
const previewStyle = document.createElement('style');
|
|
494
|
+
previewStyle.textContent = `
|
|
495
|
+
.preview-embed-placeholder {
|
|
496
|
+
margin: 0.75rem 0;
|
|
497
|
+
padding: 0.75rem;
|
|
498
|
+
border: 1px dashed #9db29a;
|
|
499
|
+
border-radius: 6px;
|
|
500
|
+
background: #f4f8f2;
|
|
501
|
+
color: #3e5740;
|
|
502
|
+
font-style: italic;
|
|
503
|
+
}
|
|
504
|
+
`;
|
|
505
|
+
docAny.head?.append(previewStyle);
|
|
506
|
+
return `<!doctype html>\n${document.documentElement.outerHTML}`;
|
|
507
|
+
}
|
|
508
|
+
function resolveEntryPath(docPath, reference, entries) {
|
|
509
|
+
const raw = reference.trim();
|
|
510
|
+
if (!raw || raw.startsWith('#') || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(raw)) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
const normalizedReference = raw.split('#')[0].split('?')[0];
|
|
514
|
+
if (!normalizedReference) {
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
const baseDir = docPath.includes('/') ? docPath.slice(0, docPath.lastIndexOf('/') + 1) : '';
|
|
518
|
+
const combined = normalizedReference.startsWith('/') ? normalizedReference.slice(1) : `${baseDir}${normalizedReference}`;
|
|
519
|
+
const normalized = normalizeEntryPath(combined);
|
|
520
|
+
const candidates = [
|
|
521
|
+
normalized,
|
|
522
|
+
normalized.startsWith('html/') ? normalized.slice(5) : '',
|
|
523
|
+
normalized.startsWith('content/') ? normalized.slice(8) : '',
|
|
524
|
+
`content/${normalized}`,
|
|
525
|
+
].filter(Boolean);
|
|
526
|
+
for (const candidate of candidates) {
|
|
527
|
+
if (entries[candidate]) {
|
|
528
|
+
return candidate;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
function normalizeEntryPath(path) {
|
|
534
|
+
const parts = path.replaceAll('\\', '/').split('/');
|
|
535
|
+
const normalized = [];
|
|
536
|
+
for (const part of parts) {
|
|
537
|
+
if (!part || part === '.') {
|
|
538
|
+
continue;
|
|
539
|
+
}
|
|
540
|
+
if (part === '..') {
|
|
541
|
+
normalized.pop();
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
normalized.push(part);
|
|
545
|
+
}
|
|
546
|
+
return normalized.join('/');
|
|
547
|
+
}
|
|
548
|
+
function uint8ToBase64(data) {
|
|
549
|
+
let binary = '';
|
|
550
|
+
const chunkSize = 0x8000;
|
|
551
|
+
for (let index = 0; index < data.length; index += chunkSize) {
|
|
552
|
+
const chunk = data.subarray(index, index + chunkSize);
|
|
553
|
+
binary += String.fromCharCode(...chunk);
|
|
554
|
+
}
|
|
555
|
+
return btoa(binary);
|
|
556
|
+
}
|
|
557
|
+
function getMimeTypeFromPath(path) {
|
|
558
|
+
const lower = path.toLowerCase();
|
|
559
|
+
if (lower.endsWith('.css'))
|
|
560
|
+
return 'text/css';
|
|
561
|
+
if (lower.endsWith('.js'))
|
|
562
|
+
return 'application/javascript';
|
|
563
|
+
if (lower.endsWith('.svg'))
|
|
564
|
+
return 'image/svg+xml';
|
|
565
|
+
if (lower.endsWith('.png'))
|
|
566
|
+
return 'image/png';
|
|
567
|
+
if (lower.endsWith('.jpg') || lower.endsWith('.jpeg'))
|
|
568
|
+
return 'image/jpeg';
|
|
569
|
+
if (lower.endsWith('.gif'))
|
|
570
|
+
return 'image/gif';
|
|
571
|
+
if (lower.endsWith('.webp'))
|
|
572
|
+
return 'image/webp';
|
|
573
|
+
if (lower.endsWith('.mp3'))
|
|
574
|
+
return 'audio/mpeg';
|
|
575
|
+
if (lower.endsWith('.ogg'))
|
|
576
|
+
return 'audio/ogg';
|
|
577
|
+
if (lower.endsWith('.wav'))
|
|
578
|
+
return 'audio/wav';
|
|
579
|
+
if (lower.endsWith('.mp4'))
|
|
580
|
+
return 'video/mp4';
|
|
581
|
+
if (lower.endsWith('.webm'))
|
|
582
|
+
return 'video/webm';
|
|
583
|
+
if (lower.endsWith('.woff2'))
|
|
584
|
+
return 'font/woff2';
|
|
585
|
+
if (lower.endsWith('.woff'))
|
|
586
|
+
return 'font/woff';
|
|
587
|
+
if (lower.endsWith('.ttf'))
|
|
588
|
+
return 'font/ttf';
|
|
589
|
+
if (lower.endsWith('.ico'))
|
|
590
|
+
return 'image/x-icon';
|
|
591
|
+
return 'application/octet-stream';
|
|
592
|
+
}
|
|
593
|
+
function inlineCssAssetUrls(cssText, cssPath, entries) {
|
|
594
|
+
return cssText.replace(/url\(([^)]+)\)/gi, (fullMatch, rawReference) => {
|
|
595
|
+
const reference = rawReference.trim().replace(/^['"]|['"]$/g, '');
|
|
596
|
+
if (!reference || reference.startsWith('data:') || reference.startsWith('#')) {
|
|
597
|
+
return fullMatch;
|
|
598
|
+
}
|
|
599
|
+
const resolved = resolveEntryPath(cssPath, reference, entries);
|
|
600
|
+
if (!resolved) {
|
|
601
|
+
return fullMatch;
|
|
602
|
+
}
|
|
603
|
+
const mime = getMimeTypeFromPath(resolved);
|
|
604
|
+
const dataUrl = `data:${mime};base64,${uint8ToBase64(entries[resolved])}`;
|
|
605
|
+
return `url("${dataUrl}")`;
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
function generateContentXml(project) {
|
|
609
|
+
const odeId = createResourceId();
|
|
610
|
+
const odeVersionId = createResourceId();
|
|
611
|
+
const modified = String(Date.now());
|
|
612
|
+
const generatedAtIso = new Date().toISOString();
|
|
613
|
+
const pageIds = project.pages.map(() => createPageId());
|
|
614
|
+
const navStructuresXml = project.pages
|
|
615
|
+
.map((page, index) => generateOdeNavStructureXml(page, index, pageIds))
|
|
616
|
+
.join('');
|
|
617
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
618
|
+
<!DOCTYPE ode SYSTEM "content.dtd">
|
|
619
|
+
<ode xmlns="http://www.intef.es/xsd/ode" version="2.0">
|
|
620
|
+
<userPreferences>
|
|
621
|
+
<userPreference>
|
|
622
|
+
<key>theme</key>
|
|
623
|
+
<value>base</value>
|
|
624
|
+
</userPreference>
|
|
625
|
+
</userPreferences>
|
|
626
|
+
<odeResources>
|
|
627
|
+
<odeResource><key>odeId</key><value>${escapeXml(odeId)}</value></odeResource>
|
|
628
|
+
<odeResource><key>odeVersionId</key><value>${escapeXml(odeVersionId)}</value></odeResource>
|
|
629
|
+
<odeResource><key>exe_version</key><value>3.0</value></odeResource>
|
|
630
|
+
</odeResources>
|
|
631
|
+
<odeProperties>
|
|
632
|
+
<odeProperty><key>pp_title</key><value>${escapeXml(project.title || 'Documento importado')}</value></odeProperty>
|
|
633
|
+
<odeProperty><key>pp_subtitle</key><value>${escapeXml(project.subtitle || '')}</value></odeProperty>
|
|
634
|
+
<odeProperty><key>pp_lang</key><value>es</value></odeProperty>
|
|
635
|
+
<odeProperty><key>pp_license</key><value>creative commons: attribution - share alike 4.0</value></odeProperty>
|
|
636
|
+
<odeProperty><key>pp_licenseUrl</key><value>https://creativecommons.org/licenses/by-sa/4.0/</value></odeProperty>
|
|
637
|
+
<odeProperty><key>pp_theme</key><value>base</value></odeProperty>
|
|
638
|
+
<odeProperty><key>pp_exelearning_version</key><value>v4.0.0-beta1</value></odeProperty>
|
|
639
|
+
<odeProperty><key>pp_modified</key><value>${escapeXml(modified)}</value></odeProperty>
|
|
640
|
+
<odeProperty><key>pp_addExeLink</key><value>false</value></odeProperty>
|
|
641
|
+
<odeProperty><key>pp_addPagination</key><value>true</value></odeProperty>
|
|
642
|
+
<odeProperty><key>pp_addSearchBox</key><value>false</value></odeProperty>
|
|
643
|
+
<odeProperty><key>pp_addAccessibilityToolbar</key><value>false</value></odeProperty>
|
|
644
|
+
<odeProperty><key>pp_addMathJax</key><value>true</value></odeProperty>
|
|
645
|
+
<odeProperty><key>exportSource</key><value>true</value></odeProperty>
|
|
646
|
+
<odeProperty><key>pp_globalFont</key><value>default</value></odeProperty>
|
|
647
|
+
<odeProperty><key>execonvert_generator</key><value>${escapeXml(EXECONVERT_SIGNATURE)}</value></odeProperty>
|
|
648
|
+
<odeProperty><key>execonvert_generatedAt</key><value>${escapeXml(generatedAtIso)}</value></odeProperty>
|
|
649
|
+
</odeProperties>
|
|
650
|
+
<odeNavStructures>
|
|
651
|
+
${navStructuresXml}</odeNavStructures>
|
|
652
|
+
</ode>`;
|
|
653
|
+
}
|
|
654
|
+
function generateOdeNavStructureXml(page, order, pageIds) {
|
|
655
|
+
const pageId = pageIds[order];
|
|
656
|
+
const parentPageId = page.parentIndex === null ? '' : pageIds[page.parentIndex] || '';
|
|
657
|
+
const title = page.title || `Página ${order + 1}`;
|
|
658
|
+
const blocksXml = page.blocks.map((block, index) => generateOdePagStructureXml(block, pageId, index)).join('');
|
|
659
|
+
return `<odeNavStructure>
|
|
660
|
+
<odePageId>${escapeXml(pageId)}</odePageId>
|
|
661
|
+
<odeParentPageId>${escapeXml(parentPageId)}</odeParentPageId>
|
|
662
|
+
<pageName>${escapeXml(title)}</pageName>
|
|
663
|
+
<odeNavStructureOrder>${order}</odeNavStructureOrder>
|
|
664
|
+
<odeNavStructureProperties>
|
|
665
|
+
${generateNavStructurePropertyEntry('titlePage', title)}${generateNavStructurePropertyEntry('titleNode', title)}${generateNavStructurePropertyEntry('hidePageTitle', 'false')}${generateNavStructurePropertyEntry('titleHtml', '')}${generateNavStructurePropertyEntry('editableInPage', 'false')}${generateNavStructurePropertyEntry('visibility', 'true')}${generateNavStructurePropertyEntry('highlight', 'false')}${generateNavStructurePropertyEntry('description', '')} </odeNavStructureProperties>
|
|
666
|
+
<odePagStructures>
|
|
667
|
+
${blocksXml} </odePagStructures>
|
|
668
|
+
</odeNavStructure>
|
|
669
|
+
`;
|
|
670
|
+
}
|
|
671
|
+
function generateNavStructurePropertyEntry(key, value) {
|
|
672
|
+
return ` <odeNavStructureProperty>
|
|
673
|
+
<key>${escapeXml(key)}</key>
|
|
674
|
+
<value>${escapeXml(value)}</value>
|
|
675
|
+
</odeNavStructureProperty>
|
|
676
|
+
`;
|
|
677
|
+
}
|
|
678
|
+
function generateOdePagStructureXml(block, pageId, order) {
|
|
679
|
+
const blockId = createBlockId();
|
|
680
|
+
const ideviceId = createIdeviceId();
|
|
681
|
+
const blockName = block.title || 'Contenido';
|
|
682
|
+
const html = block.html || '<p></p>';
|
|
683
|
+
const wrappedHtml = `<div class="exe-text-template">\n${html}\n</div>`;
|
|
684
|
+
const jsonProperties = JSON.stringify({
|
|
685
|
+
ideviceId,
|
|
686
|
+
textInfoDurationInput: '',
|
|
687
|
+
textInfoDurationTextInput: 'Duración',
|
|
688
|
+
textInfoParticipantsInput: '',
|
|
689
|
+
textInfoParticipantsTextInput: 'Agrupamiento',
|
|
690
|
+
textTextarea: html,
|
|
691
|
+
textFeedbackInput: 'Mostrar retroalimentación',
|
|
692
|
+
textFeedbackTextarea: '',
|
|
693
|
+
});
|
|
694
|
+
return ` <odePagStructure>
|
|
695
|
+
<odePageId>${escapeXml(pageId)}</odePageId>
|
|
696
|
+
<odeBlockId>${escapeXml(blockId)}</odeBlockId>
|
|
697
|
+
<blockName>${escapeXml(blockName)}</blockName>
|
|
698
|
+
<iconName></iconName>
|
|
699
|
+
<odePagStructureOrder>${order}</odePagStructureOrder>
|
|
700
|
+
<odePagStructureProperties>
|
|
701
|
+
${generatePagStructurePropertyEntry('visibility', 'true')}${generatePagStructurePropertyEntry('teacherOnly', 'false')}${generatePagStructurePropertyEntry('allowToggle', 'true')}${generatePagStructurePropertyEntry('minimized', 'false')}${generatePagStructurePropertyEntry('cssClass', '')} </odePagStructureProperties>
|
|
702
|
+
<odeComponents>
|
|
703
|
+
<odeComponent>
|
|
704
|
+
<odePageId>${escapeXml(pageId)}</odePageId>
|
|
705
|
+
<odeBlockId>${escapeXml(blockId)}</odeBlockId>
|
|
706
|
+
<odeIdeviceId>${escapeXml(ideviceId)}</odeIdeviceId>
|
|
707
|
+
<odeIdeviceTypeName>text</odeIdeviceTypeName>
|
|
708
|
+
<htmlView><![CDATA[${escapeCdata(wrappedHtml)}]]></htmlView>
|
|
709
|
+
<jsonProperties><![CDATA[${escapeCdata(jsonProperties)}]]></jsonProperties>
|
|
710
|
+
<odeComponentsOrder>0</odeComponentsOrder>
|
|
711
|
+
<odeComponentsProperties>
|
|
712
|
+
</odeComponentsProperties>
|
|
713
|
+
</odeComponent>
|
|
714
|
+
</odeComponents>
|
|
715
|
+
</odePagStructure>
|
|
716
|
+
`;
|
|
717
|
+
}
|
|
718
|
+
function generatePagStructurePropertyEntry(key, value) {
|
|
719
|
+
return ` <odePagStructureProperty>
|
|
720
|
+
<key>${escapeXml(key)}</key>
|
|
721
|
+
<value>${escapeXml(value)}</value>
|
|
722
|
+
</odePagStructureProperty>
|
|
723
|
+
`;
|
|
724
|
+
}
|
|
725
|
+
function addPreviewHtmlEntries(entries, project) {
|
|
726
|
+
const pages = getPreviewPages(project);
|
|
727
|
+
for (const existingPath of Object.keys(entries)) {
|
|
728
|
+
if (existingPath.startsWith('html/') && existingPath.endsWith('.html')) {
|
|
729
|
+
delete entries[existingPath];
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
for (const [index, page] of pages.entries()) {
|
|
733
|
+
const html = generatePreviewPageHtml(project, pages, index);
|
|
734
|
+
entries[page.href] = new TextEncoder().encode(html);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
function getPreviewPages(project) {
|
|
738
|
+
const used = new Set();
|
|
739
|
+
return project.pages.map((page, index) => {
|
|
740
|
+
if (index === 0) {
|
|
741
|
+
return {
|
|
742
|
+
title: page.title || 'Página 1',
|
|
743
|
+
href: 'index.html',
|
|
744
|
+
pageNumber: 1,
|
|
745
|
+
level: page.level,
|
|
746
|
+
parentIndex: page.parentIndex,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
let slug = slugifyPageTitle(page.title || `pagina-${index + 1}`);
|
|
750
|
+
if (!slug) {
|
|
751
|
+
slug = `pagina-${index + 1}`;
|
|
752
|
+
}
|
|
753
|
+
let candidate = slug;
|
|
754
|
+
let suffix = 2;
|
|
755
|
+
while (used.has(candidate)) {
|
|
756
|
+
candidate = `${slug}-${suffix}`;
|
|
757
|
+
suffix += 1;
|
|
758
|
+
}
|
|
759
|
+
used.add(candidate);
|
|
760
|
+
return {
|
|
761
|
+
title: page.title || `Página ${index + 1}`,
|
|
762
|
+
href: `html/${candidate}.html`,
|
|
763
|
+
pageNumber: index + 1,
|
|
764
|
+
level: page.level,
|
|
765
|
+
parentIndex: page.parentIndex,
|
|
766
|
+
};
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
function generatePreviewPageHtml(project, pages, activeIndex, previewOptions = {}) {
|
|
770
|
+
const activePageInfo = pages[activeIndex];
|
|
771
|
+
const activePage = project.pages[activeIndex];
|
|
772
|
+
const assetPrefix = activeIndex === 0 ? '' : '../';
|
|
773
|
+
const prevPage = activeIndex > 0 ? pages[activeIndex - 1] : null;
|
|
774
|
+
const nextPage = activeIndex < pages.length - 1 ? pages[activeIndex + 1] : null;
|
|
775
|
+
const navItems = generatePreviewNavHtml(pages, activePageInfo.pageNumber, activeIndex);
|
|
776
|
+
const blocks = activePage.blocks
|
|
777
|
+
.map((block, blockIndex) => generatePreviewBlockHtml(block, activePageInfo.pageNumber, blockIndex, previewOptions))
|
|
778
|
+
.join('\n');
|
|
779
|
+
const prevHref = prevPage
|
|
780
|
+
? activeIndex === 1
|
|
781
|
+
? '../index.html'
|
|
782
|
+
: activeIndex > 1
|
|
783
|
+
? prevPage.href.replace(/^html\//, '')
|
|
784
|
+
: prevPage.href
|
|
785
|
+
: '';
|
|
786
|
+
const nextHref = nextPage
|
|
787
|
+
? activeIndex === 0
|
|
788
|
+
? nextPage.href
|
|
789
|
+
: nextPage.pageNumber === 1
|
|
790
|
+
? '../index.html'
|
|
791
|
+
: nextPage.href.replace(/^html\//, '')
|
|
792
|
+
: '';
|
|
793
|
+
return `<!DOCTYPE html>
|
|
794
|
+
<html lang="es" id="exe-index">
|
|
795
|
+
<head>
|
|
796
|
+
<meta charset="utf-8">
|
|
797
|
+
<meta name="generator" content="eXeLearning v4.0.0-beta1">
|
|
798
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
799
|
+
<link rel="license" type="text/html" href="https://creativecommons.org/licenses/by-sa/4.0/">
|
|
800
|
+
<title>${escapeHtml(project.title || 'eXeLearning')}</title>
|
|
801
|
+
<link rel="icon" type="image/x-icon" href="${assetPrefix}libs/favicon.ico">
|
|
802
|
+
<script>document.querySelector("html").classList.add("js");</script><script src="${assetPrefix}libs/jquery/jquery.min.js"> </script><script src="${assetPrefix}libs/common_i18n.js"> </script><script src="${assetPrefix}libs/common.js"> </script><script src="${assetPrefix}libs/exe_export.js"> </script><script src="${assetPrefix}libs/bootstrap/bootstrap.bundle.min.js"> </script><link rel="stylesheet" href="${assetPrefix}libs/bootstrap/bootstrap.min.css">
|
|
803
|
+
<script>
|
|
804
|
+
window.MathJax = {
|
|
805
|
+
tex: {
|
|
806
|
+
inlineMath: [['\\\\(', '\\\\)']],
|
|
807
|
+
displayMath: [['\\\\[', '\\\\]']],
|
|
808
|
+
processEscapes: true,
|
|
809
|
+
},
|
|
810
|
+
svg: {
|
|
811
|
+
fontCache: 'global',
|
|
812
|
+
},
|
|
813
|
+
};
|
|
814
|
+
</script>
|
|
815
|
+
<script src="${assetPrefix}libs/exe_math/tex-mml-svg.js"></script>
|
|
816
|
+
<script src="${assetPrefix}idevices/text/text.js"></script><link rel="stylesheet" href="${assetPrefix}idevices/text/text.css">
|
|
817
|
+
<link rel="stylesheet" href="${assetPrefix}content/css/base.css"><script src="${assetPrefix}theme/style.js"> </script><link rel="stylesheet" href="${assetPrefix}theme/style.css">
|
|
818
|
+
<style>
|
|
819
|
+
body.exe-export.exe-web-site{min-width:0}
|
|
820
|
+
.idevice_node.text .exe-text-template>:first-child{margin-top:0}
|
|
821
|
+
.idevice_node.text .exe-text-template>:last-child{margin-bottom:0}
|
|
822
|
+
.page-content .box+.box{margin-top:1.25rem}
|
|
823
|
+
</style>
|
|
824
|
+
</head>
|
|
825
|
+
<body class="exe-export exe-web-site">
|
|
826
|
+
<script>document.body.className+=" js"</script>
|
|
827
|
+
<div class="exe-content exe-export pre-js siteNav-hidden"> <nav id="siteNav">
|
|
828
|
+
<ul>
|
|
829
|
+
${navItems}
|
|
830
|
+
</ul>
|
|
831
|
+
</nav><main id="${escapeHtml(createPageDomId(activePageInfo.pageNumber))}" class="page">
|
|
832
|
+
<header class="main-header"> <p class="page-counter"> <span class="page-counter-label">Página </span><span class="page-counter-content"> <strong class="page-counter-current-page">${activePageInfo.pageNumber}</strong><span class="page-counter-sep">/</span><strong class="page-counter-total">${pages.length}</strong></span></p>
|
|
833
|
+
|
|
834
|
+
<div class="package-header"><h1 class="package-title">${escapeHtml(project.title || 'Documento importado')}</h1></div>
|
|
835
|
+
<div class="page-header"><h2 class="page-title">${escapeHtml(activePage.title || `Página ${activePageInfo.pageNumber}`)}</h2></div>
|
|
836
|
+
</header><div id="page-content-${escapeHtml(createPageDomId(activePageInfo.pageNumber))}" class="page-content">
|
|
837
|
+
${blocks}
|
|
838
|
+
</div></main><div class="nav-buttons">
|
|
839
|
+
${prevPage ? `<a href="${escapeHtml(prevHref)}" title="Previous" class="nav-button nav-button-left"><span>Previous</span></a>` : '<span class="nav-button nav-button-left" aria-hidden="true"><span>Previous</span></span>'}
|
|
840
|
+
${nextPage ? `<a href="${escapeHtml(nextHref)}" title="Next" class="nav-button nav-button-right"><span>Next</span></a>` : '<span class="nav-button nav-button-right" aria-hidden="true"><span>Next</span></span>'}
|
|
841
|
+
</div>
|
|
842
|
+
<footer id="siteFooter"><div id="siteFooterContent"> <div id="packageLicense" class="cc cc-by-sa"> <p> <span class="license-label">Licencia: </span><a href="https://creativecommons.org/licenses/by-sa/4.0/" class="license">creative commons: attribution - share alike 4.0 (BY-SA)</a></p>
|
|
843
|
+
</div>
|
|
844
|
+
</div></footer>
|
|
845
|
+
</div>
|
|
846
|
+
|
|
847
|
+
</body>
|
|
848
|
+
</html>`;
|
|
849
|
+
}
|
|
850
|
+
function generatePreviewNavHtml(pages, activePageNumber, activeIndex) {
|
|
851
|
+
const buildHref = (pageInfo) => {
|
|
852
|
+
if (activeIndex === 0) {
|
|
853
|
+
return pageInfo.href;
|
|
854
|
+
}
|
|
855
|
+
return pageInfo.pageNumber === 1 ? `../${pageInfo.href}` : pageInfo.href.replace(/^html\//, '');
|
|
856
|
+
};
|
|
857
|
+
const renderBranch = (parentIndex) => {
|
|
858
|
+
const branchPages = pages.filter((page, index) => page.parentIndex === parentIndex && index !== parentIndex);
|
|
859
|
+
return branchPages
|
|
860
|
+
.map((pageInfo, index) => {
|
|
861
|
+
const pageIndex = pages.findIndex(candidate => candidate.pageNumber === pageInfo.pageNumber);
|
|
862
|
+
const active = pageInfo.pageNumber === activePageNumber;
|
|
863
|
+
const classes = [
|
|
864
|
+
active ? 'active' : '',
|
|
865
|
+
pageInfo.pageNumber === 1 ? 'root-node' : '',
|
|
866
|
+
`nav-level-${pageInfo.level}`,
|
|
867
|
+
]
|
|
868
|
+
.filter(Boolean)
|
|
869
|
+
.join(' ');
|
|
870
|
+
const childrenHtml = renderBranch(pageIndex);
|
|
871
|
+
const childList = childrenHtml ? `\n<ul>\n${childrenHtml}\n</ul>` : '';
|
|
872
|
+
return `<li${active ? ' class="active"' : ''}><a href="${escapeHtml(buildHref(pageInfo))}" class="${escapeHtml(`${classes} no-ch`.trim())}">${escapeHtml(pageInfo.title)}</a>${childList}</li>`;
|
|
873
|
+
})
|
|
874
|
+
.join('\n');
|
|
875
|
+
};
|
|
876
|
+
return renderBranch(null);
|
|
877
|
+
}
|
|
878
|
+
function generatePreviewBlockHtml(block, pageNumber, blockIndex, previewOptions = {}) {
|
|
879
|
+
const blockId = `block-preview-${pageNumber}-${blockIndex + 1}`;
|
|
880
|
+
const ideviceId = `idevice-preview-${pageNumber}-${blockIndex + 1}`;
|
|
881
|
+
const sanitizedHtml = sanitizePreviewBlockHtml(block.html || '<p></p>');
|
|
882
|
+
const safeHtml = previewOptions.renderLatex ? renderLatexPreviewHtml(sanitizedHtml) : sanitizedHtml;
|
|
883
|
+
return `<article id="${escapeHtml(blockId)}" class="box">
|
|
884
|
+
<header class="box-head no-icon">
|
|
885
|
+
<h1 class="box-title">${escapeHtml(block.title || 'Contenido')}</h1>
|
|
886
|
+
<button class="box-toggle box-toggle-on" title="Toggle content">
|
|
887
|
+
<span>Toggle content</span>
|
|
888
|
+
</button></header>
|
|
889
|
+
<div class="box-content">
|
|
890
|
+
<div id="${escapeHtml(ideviceId)}" class="idevice_node text" data-idevice-path="idevices/text/" data-idevice-type="text" data-idevice-component-type="json" data-idevice-json-data="{"ideviceId":"${escapeHtml(ideviceId)}"}">
|
|
891
|
+
<div class="exe-text"><div class="exe-text-template">
|
|
892
|
+
${safeHtml}
|
|
893
|
+
</div></div>
|
|
894
|
+
</div>
|
|
895
|
+
</div>
|
|
896
|
+
</article>`;
|
|
897
|
+
}
|
|
898
|
+
function createPageDomId(pageNumber) {
|
|
899
|
+
return `page-preview-${pageNumber}`;
|
|
900
|
+
}
|
|
901
|
+
function slugifyPageTitle(value) {
|
|
902
|
+
return value
|
|
903
|
+
.normalize('NFD')
|
|
904
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
905
|
+
.toLowerCase()
|
|
906
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
907
|
+
.replace(/^-+|-+$/g, '');
|
|
908
|
+
}
|
|
909
|
+
function sanitizePreviewBlockHtml(html) {
|
|
910
|
+
const document = new DOMParser().parseFromString(`<!doctype html><html><body>${html}</body></html>`, 'text/html');
|
|
911
|
+
const body = document.body;
|
|
912
|
+
if (!body || !('querySelectorAll' in body)) {
|
|
913
|
+
return html;
|
|
914
|
+
}
|
|
915
|
+
for (const element of Array.from(body.querySelectorAll('script, iframe, object, embed'))) {
|
|
916
|
+
const replacement = document.createElement('div');
|
|
917
|
+
replacement.className = 'preview-embed-placeholder';
|
|
918
|
+
replacement.textContent = 'Contenido incrustado omitido en la vista previa.';
|
|
919
|
+
element.replaceWith(replacement);
|
|
920
|
+
}
|
|
921
|
+
for (const anchor of Array.from(body.querySelectorAll('a[href]'))) {
|
|
922
|
+
const href = (anchor.getAttribute('href') || '').trim();
|
|
923
|
+
if (!href || href.startsWith('#') || /^(?:[a-z][a-z0-9+.-]*:|\/\/)/i.test(href)) {
|
|
924
|
+
continue;
|
|
925
|
+
}
|
|
926
|
+
anchor.setAttribute('href', '#');
|
|
927
|
+
anchor.removeAttribute('target');
|
|
928
|
+
}
|
|
929
|
+
return body.innerHTML || '<p></p>';
|
|
930
|
+
}
|
|
931
|
+
function renderLatexPreviewHtml(html) {
|
|
932
|
+
return html
|
|
933
|
+
.replace(/\\\[([\s\S]*?)\\\]/g, (_match, expression) => renderLatexFragment(expression, true))
|
|
934
|
+
.replace(/\\\(([\s\S]*?)\\\)/g, (_match, expression) => renderLatexFragment(expression, false));
|
|
935
|
+
}
|
|
936
|
+
function renderLatexFragment(expression, displayMode) {
|
|
937
|
+
const normalized = normalizeLegacyPreviewLatex(expression);
|
|
938
|
+
if (!normalized) {
|
|
939
|
+
return displayMode ? '\\[\\]' : '\\(\\)';
|
|
940
|
+
}
|
|
941
|
+
try {
|
|
942
|
+
return temml.renderToString(normalized, {
|
|
943
|
+
displayMode,
|
|
944
|
+
throwOnError: false,
|
|
945
|
+
annotate: false,
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
catch {
|
|
949
|
+
return displayMode ? `\\[${normalized}\\]` : `\\(${normalized}\\)`;
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
function normalizeLegacyPreviewLatex(expression) {
|
|
953
|
+
const htmlLike = expression
|
|
954
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
955
|
+
.replace(/ /gi, ' ')
|
|
956
|
+
.replace(/&/gi, '&')
|
|
957
|
+
.replace(/</gi, '<')
|
|
958
|
+
.replace(/>/gi, '>');
|
|
959
|
+
const parsed = new DOMParser().parseFromString(`<!doctype html><html><body>${htmlLike}</body></html>`, 'text/html');
|
|
960
|
+
const body = parsed.body;
|
|
961
|
+
const text = body?.textContent || htmlLike;
|
|
962
|
+
return text
|
|
963
|
+
.normalize('NFC')
|
|
964
|
+
.replace(/\u00a0/g, ' ')
|
|
965
|
+
.replace(/[\uFFFD\uFEFF\u00AD\u2066-\u2069\u200B-\u200F\u202A-\u202E\uFFF9-\uFFFB]/g, '')
|
|
966
|
+
.replace(/[\uD800-\uDFFF]/g, '')
|
|
967
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '')
|
|
968
|
+
.replace(/[ \t]*\n[ \t]*/g, '\n')
|
|
969
|
+
.replace(/[ \t]+/g, ' ')
|
|
970
|
+
.trim();
|
|
971
|
+
}
|
|
972
|
+
function decodeUtf8(data) {
|
|
973
|
+
return data ? new TextDecoder().decode(data) : '';
|
|
974
|
+
}
|
|
975
|
+
function parseXml(xml, errorMessage) {
|
|
976
|
+
const document = new DOMParser().parseFromString(xml, 'application/xml');
|
|
977
|
+
const parserError = document.querySelector('parsererror');
|
|
978
|
+
if (parserError) {
|
|
979
|
+
throw new Error(errorMessage);
|
|
980
|
+
}
|
|
981
|
+
return document;
|
|
982
|
+
}
|
|
983
|
+
function normalizeWhitespace(value) {
|
|
984
|
+
return value.replace(/\s+/g, ' ').trim();
|
|
985
|
+
}
|
|
986
|
+
function normalizeMathMarkup(html) {
|
|
987
|
+
const replaceMath = (fullMatch, content, open, close) => {
|
|
988
|
+
const normalized = normalizeLatexValue(stripHtmlFromMath(content));
|
|
989
|
+
if (!normalized) {
|
|
990
|
+
return fullMatch;
|
|
991
|
+
}
|
|
992
|
+
if (!looksLikeMathExpression(normalized)) {
|
|
993
|
+
return fullMatch;
|
|
994
|
+
}
|
|
995
|
+
return `${open}${normalized}${close}`;
|
|
996
|
+
};
|
|
997
|
+
const withInlineMath = html.replace(/\\\(([\s\S]*?)\\\)/g, (fullMatch, content) => replaceMath(fullMatch, content, '\\(', '\\)'));
|
|
998
|
+
return withInlineMath.replace(/\\\[([\s\S]*?)\\\]/g, (fullMatch, content) => replaceMath(fullMatch, content, '\\[', '\\]'));
|
|
999
|
+
}
|
|
1000
|
+
function stripHtmlFromMath(value) {
|
|
1001
|
+
const withLineBreaks = value
|
|
1002
|
+
.replace(/<br\s*\/?>/gi, '\n')
|
|
1003
|
+
.replace(/<\/p>\s*<p>/gi, '\n')
|
|
1004
|
+
.replace(/<\/div>\s*<div>/gi, '\n');
|
|
1005
|
+
const document = new DOMParser().parseFromString(`<!doctype html><html><body>${withLineBreaks}</body></html>`, 'text/html');
|
|
1006
|
+
return document.body.textContent || '';
|
|
1007
|
+
}
|
|
1008
|
+
function normalizeLatexValue(value) {
|
|
1009
|
+
let output = value.normalize('NFC').replace(/\u00a0/g, ' ').replace(/\r/g, '');
|
|
1010
|
+
output = output.replace(/[\uFFFD\uFEFF\u00AD\u2066-\u2069\u200B-\u200F\u202A-\u202E\uFFF9-\uFFFB]/g, '');
|
|
1011
|
+
output = output.replace(/[\uD800-\uDFFF]/g, '');
|
|
1012
|
+
output = output.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, '');
|
|
1013
|
+
output = output.replace(/[ \t]*\n[ \t]*/g, '\n');
|
|
1014
|
+
output = output.replace(/\\\s+([A-Za-z])/g, '\\$1');
|
|
1015
|
+
output = output.replace(/\\ext\{/g, '\\text{');
|
|
1016
|
+
output = output.replace(/\bL\s+A\s+T\s+E\s+X\b\\?/g, '\\LaTeX');
|
|
1017
|
+
output = output.replace(/(^|[^\\])\.{2}\s+\\\\/g, '$1\\ldots ');
|
|
1018
|
+
output = output.replace(/\\\\backslash\b/g, '\\\\');
|
|
1019
|
+
output = output.replace(/\\backslash\b/g, '\\\\');
|
|
1020
|
+
output = output.replace(/[ \t]+/g, ' ');
|
|
1021
|
+
output = output.replace(/ ?\n ?/g, '\n');
|
|
1022
|
+
output = output.trim();
|
|
1023
|
+
while (output.endsWith('\\')) {
|
|
1024
|
+
output = output.slice(0, -1).trimEnd();
|
|
1025
|
+
}
|
|
1026
|
+
return output;
|
|
1027
|
+
}
|
|
1028
|
+
function looksLikeMathExpression(value) {
|
|
1029
|
+
if (!value || value.length > 8000) {
|
|
1030
|
+
return false;
|
|
1031
|
+
}
|
|
1032
|
+
if (/^(?:[A-Za-z]\s+){1,}[A-Za-z]$/.test(value)) {
|
|
1033
|
+
return true;
|
|
1034
|
+
}
|
|
1035
|
+
if (/[0-9_^=+\-*/<>|&{}]/.test(value)) {
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
if (/\\[A-Za-z]+/.test(value)) {
|
|
1039
|
+
return true;
|
|
1040
|
+
}
|
|
1041
|
+
if (/^[A-Za-z]$/.test(value)) {
|
|
1042
|
+
return true;
|
|
1043
|
+
}
|
|
1044
|
+
return false;
|
|
1045
|
+
}
|
|
1046
|
+
function escapeHtml(value) {
|
|
1047
|
+
return value
|
|
1048
|
+
.replace(/&/g, '&')
|
|
1049
|
+
.replace(/</g, '<')
|
|
1050
|
+
.replace(/>/g, '>')
|
|
1051
|
+
.replace(/"/g, '"')
|
|
1052
|
+
.replace(/'/g, ''');
|
|
1053
|
+
}
|
|
1054
|
+
function escapeXml(value) {
|
|
1055
|
+
return escapeHtml(value);
|
|
1056
|
+
}
|
|
1057
|
+
function escapeCdata(value) {
|
|
1058
|
+
return value.replaceAll(']]>', ']]]]><![CDATA[>');
|
|
1059
|
+
}
|
|
1060
|
+
function stemFromFilename(filename) {
|
|
1061
|
+
return filename.replace(/\.[^.]+$/, '').trim();
|
|
1062
|
+
}
|
|
1063
|
+
function toElpxFilename(inputFilename) {
|
|
1064
|
+
const stem = stemFromFilename(inputFilename) || 'proyecto';
|
|
1065
|
+
return `${stem}.elpx`;
|
|
1066
|
+
}
|
|
1067
|
+
function createPageId() {
|
|
1068
|
+
return crypto.randomUUID();
|
|
1069
|
+
}
|
|
1070
|
+
function createBlockId() {
|
|
1071
|
+
return `block-${Date.now()}-${randomSuffix()}`;
|
|
1072
|
+
}
|
|
1073
|
+
function createIdeviceId() {
|
|
1074
|
+
return `idevice-${Date.now()}-${randomSuffix()}`;
|
|
1075
|
+
}
|
|
1076
|
+
function createResourceId() {
|
|
1077
|
+
return `${timestampStamp()}${randomUppercase(6)}`;
|
|
1078
|
+
}
|
|
1079
|
+
function timestampStamp() {
|
|
1080
|
+
const now = new Date();
|
|
1081
|
+
return [
|
|
1082
|
+
now.getFullYear(),
|
|
1083
|
+
String(now.getMonth() + 1).padStart(2, '0'),
|
|
1084
|
+
String(now.getDate()).padStart(2, '0'),
|
|
1085
|
+
String(now.getHours()).padStart(2, '0'),
|
|
1086
|
+
String(now.getMinutes()).padStart(2, '0'),
|
|
1087
|
+
String(now.getSeconds()).padStart(2, '0'),
|
|
1088
|
+
].join('');
|
|
1089
|
+
}
|
|
1090
|
+
function randomSuffix(length = 9) {
|
|
1091
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
1092
|
+
let output = '';
|
|
1093
|
+
for (let index = 0; index < length; index += 1) {
|
|
1094
|
+
output += chars[Math.floor(Math.random() * chars.length)];
|
|
1095
|
+
}
|
|
1096
|
+
return output;
|
|
1097
|
+
}
|
|
1098
|
+
function randomUppercase(length) {
|
|
1099
|
+
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
1100
|
+
let output = '';
|
|
1101
|
+
for (let index = 0; index < length; index += 1) {
|
|
1102
|
+
output += chars[Math.floor(Math.random() * chars.length)];
|
|
1103
|
+
}
|
|
1104
|
+
return output;
|
|
1105
|
+
}
|
|
1106
|
+
const TEXT_IDEVICE_ASSETS = ['idevices/text/text.js', 'idevices/text/text.css', 'idevices/text/text.html'];
|
|
1107
|
+
const EXE_MATH_ASSET_ARCHIVE = 'exe_math_assets.zip';
|
|
1108
|
+
const DOCX_STYLE_MAP = [
|
|
1109
|
+
"p[style-name='Code'] => pre:fresh",
|
|
1110
|
+
"p[style-name='Código'] => pre:fresh",
|
|
1111
|
+
"p[style-name='Codigo'] => pre:fresh",
|
|
1112
|
+
"p[style-name='HTML'] => pre:fresh",
|
|
1113
|
+
"p[style-name='Preformatted'] => pre:fresh",
|
|
1114
|
+
"p[style-name='Preformatted Text'] => pre:fresh",
|
|
1115
|
+
"r[style-name='Code'] => code",
|
|
1116
|
+
"r[style-name='Código'] => code",
|
|
1117
|
+
"r[style-name='Codigo'] => code",
|
|
1118
|
+
"r[style-name='HTML'] => code",
|
|
1119
|
+
];
|
|
1120
|
+
const W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main';
|
|
1121
|
+
const M_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/math';
|