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,846 @@
|
|
|
1
|
+
import { unzipSync } from 'fflate';
|
|
2
|
+
import * as Y from 'yjs';
|
|
3
|
+
const EXELEARNING_BASE_PATH = 'exelearning';
|
|
4
|
+
const APP_VERSION = 'v0.2.2';
|
|
5
|
+
const MIME_TYPES = {
|
|
6
|
+
png: 'image/png',
|
|
7
|
+
jpg: 'image/jpeg',
|
|
8
|
+
jpeg: 'image/jpeg',
|
|
9
|
+
gif: 'image/gif',
|
|
10
|
+
svg: 'image/svg+xml',
|
|
11
|
+
webp: 'image/webp',
|
|
12
|
+
bmp: 'image/bmp',
|
|
13
|
+
ico: 'image/x-icon',
|
|
14
|
+
tif: 'image/tiff',
|
|
15
|
+
tiff: 'image/tiff',
|
|
16
|
+
mp4: 'video/mp4',
|
|
17
|
+
m4v: 'video/mp4',
|
|
18
|
+
webm: 'video/webm',
|
|
19
|
+
mov: 'video/quicktime',
|
|
20
|
+
avi: 'video/x-msvideo',
|
|
21
|
+
mkv: 'video/x-matroska',
|
|
22
|
+
ogg: 'video/ogg',
|
|
23
|
+
ogv: 'video/ogg',
|
|
24
|
+
mp3: 'audio/mpeg',
|
|
25
|
+
m4a: 'audio/mp4',
|
|
26
|
+
wav: 'audio/wav',
|
|
27
|
+
aac: 'audio/aac',
|
|
28
|
+
flac: 'audio/flac',
|
|
29
|
+
pdf: 'application/pdf',
|
|
30
|
+
doc: 'application/msword',
|
|
31
|
+
docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
|
32
|
+
xls: 'application/vnd.ms-excel',
|
|
33
|
+
xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
34
|
+
ppt: 'application/vnd.ms-powerpoint',
|
|
35
|
+
pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
|
|
36
|
+
zip: 'application/zip',
|
|
37
|
+
rar: 'application/vnd.rar',
|
|
38
|
+
'7z': 'application/x-7z-compressed',
|
|
39
|
+
gltf: 'model/gltf+json',
|
|
40
|
+
glb: 'model/gltf-binary',
|
|
41
|
+
stl: 'model/stl',
|
|
42
|
+
css: 'text/css',
|
|
43
|
+
js: 'application/javascript',
|
|
44
|
+
txt: 'text/plain',
|
|
45
|
+
html: 'text/html',
|
|
46
|
+
htm: 'text/html',
|
|
47
|
+
json: 'application/json',
|
|
48
|
+
csv: 'text/csv',
|
|
49
|
+
rtf: 'application/rtf',
|
|
50
|
+
odt: 'application/vnd.oasis.opendocument.text',
|
|
51
|
+
ods: 'application/vnd.oasis.opendocument.spreadsheet',
|
|
52
|
+
odp: 'application/vnd.oasis.opendocument.presentation',
|
|
53
|
+
epub: 'application/epub+zip',
|
|
54
|
+
mobi: 'application/x-mobipocket-ebook',
|
|
55
|
+
woff: 'font/woff',
|
|
56
|
+
woff2: 'font/woff2',
|
|
57
|
+
eot: 'application/vnd.ms-fontobject',
|
|
58
|
+
ttf: 'font/ttf',
|
|
59
|
+
otf: 'font/otf',
|
|
60
|
+
xml: 'application/xml',
|
|
61
|
+
};
|
|
62
|
+
const IDEVICE_TYPE_MAP = {
|
|
63
|
+
text: 'text',
|
|
64
|
+
freetext: 'text',
|
|
65
|
+
textactivity: 'text',
|
|
66
|
+
fileattach: 'download-source-file',
|
|
67
|
+
fileattachactivity: 'download-source-file',
|
|
68
|
+
downloadpackage: 'download-source-file',
|
|
69
|
+
'download-package': 'download-source-file',
|
|
70
|
+
downloadsourcefile: 'download-source-file',
|
|
71
|
+
casestudy: 'casestudy',
|
|
72
|
+
multichoice: 'quiz',
|
|
73
|
+
truefalse: 'trueorfalse',
|
|
74
|
+
gallery: 'image-gallery',
|
|
75
|
+
imagegallery: 'image-gallery',
|
|
76
|
+
imagemagnifier: 'magnifier',
|
|
77
|
+
wikipedia: 'wikipedia',
|
|
78
|
+
rss: 'rss',
|
|
79
|
+
fill: 'fill',
|
|
80
|
+
dropdown: 'dropdown',
|
|
81
|
+
geogebra: 'geogebra-activity',
|
|
82
|
+
interactivevideo: 'interactive-video',
|
|
83
|
+
quiz: 'quick-questions',
|
|
84
|
+
quizactivity: 'quick-questions',
|
|
85
|
+
nota: 'udl-content',
|
|
86
|
+
};
|
|
87
|
+
let bundleLoadPromise = null;
|
|
88
|
+
let previewObjectUrls = [];
|
|
89
|
+
const loadedScriptUrls = new Set();
|
|
90
|
+
export async function convertElpToElpx(file, onProgress) {
|
|
91
|
+
await ensureExelearningBundles();
|
|
92
|
+
onProgress?.({ phase: 'read', message: 'Leyendo el archivo .elp...', messageKey: 'progress.readElp' });
|
|
93
|
+
const input = new Uint8Array(await file.arrayBuffer());
|
|
94
|
+
onProgress?.({ phase: 'parse', message: 'Importando el .elp con el pipeline de eXeLearning...', messageKey: 'progress.parseLegacyElp' });
|
|
95
|
+
const ydoc = new Y.Doc();
|
|
96
|
+
const assets = new MemoryAssetStore();
|
|
97
|
+
const assetHandler = new MemoryAssetHandler(assets);
|
|
98
|
+
const importerCtor = windowProxy().ElpxImporterCore;
|
|
99
|
+
if (!importerCtor) {
|
|
100
|
+
throw new Error('No se ha podido inicializar el importador de eXeLearning.');
|
|
101
|
+
}
|
|
102
|
+
const importer = new importerCtor(ydoc, assetHandler);
|
|
103
|
+
const importResult = await importer.importFromBuffer(input, {
|
|
104
|
+
clearExisting: true,
|
|
105
|
+
onProgress: (progress) => {
|
|
106
|
+
if (progress.phase === 'decompress') {
|
|
107
|
+
onProgress?.({ phase: 'parse', message: progress.message, messageKey: 'progress.parseLegacyElp' });
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (progress.phase === 'assets') {
|
|
111
|
+
onProgress?.({ phase: 'template', message: progress.message, messageKey: 'progress.applyTemplate' });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
onProgress?.({ phase: 'parse', message: progress.message, messageKey: 'progress.parseLegacyElp' });
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
onProgress?.({ phase: 'template', message: 'Generando el archivo .elpx con el exportador de eXeLearning...', messageKey: 'progress.applyTemplate' });
|
|
118
|
+
const documentManager = new MemoryDocumentManager(ydoc, assets.projectId);
|
|
119
|
+
const resources = new BundleResourceFetcher();
|
|
120
|
+
const createExporter = windowProxy().createExporter;
|
|
121
|
+
if (!createExporter) {
|
|
122
|
+
throw new Error('No se ha podido inicializar el exportador de eXeLearning.');
|
|
123
|
+
}
|
|
124
|
+
const exporter = createExporter('elpx', documentManager, null, resources, assets);
|
|
125
|
+
const exportResult = await exporter.export({ filename: toElpxFilename(file.name) });
|
|
126
|
+
if (!exportResult.success || !exportResult.data) {
|
|
127
|
+
throw new Error(exportResult.error || 'No se ha podido generar el archivo .elpx.');
|
|
128
|
+
}
|
|
129
|
+
const blob = exportResult.data instanceof Blob ? exportResult.data : new Blob([toBlobPart(exportResult.data)], { type: 'application/zip' });
|
|
130
|
+
const blobBytes = new Uint8Array(await blob.arrayBuffer());
|
|
131
|
+
onProgress?.({ phase: 'pack', message: 'Generando el archivo .elpx...', messageKey: 'progress.packElpx' });
|
|
132
|
+
const previewPages = buildPreviewPagesFromElpx(blobBytes);
|
|
133
|
+
const previewHtml = previewPages['index.html'] ||
|
|
134
|
+
'<!doctype html><html lang="es"><body><p>No se ha podido generar la previsualización.</p></body></html>';
|
|
135
|
+
return {
|
|
136
|
+
blob,
|
|
137
|
+
filename: toElpxFilename(file.name),
|
|
138
|
+
pageCount: importResult.pages,
|
|
139
|
+
blockCount: importResult.blocks,
|
|
140
|
+
previewHtml,
|
|
141
|
+
previewPages,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
async function ensureExelearningBundles() {
|
|
145
|
+
if (bundleLoadPromise) {
|
|
146
|
+
return bundleLoadPromise;
|
|
147
|
+
}
|
|
148
|
+
bundleLoadPromise = (async () => {
|
|
149
|
+
const win = windowProxy();
|
|
150
|
+
win.Y = Y;
|
|
151
|
+
win.eXeLearning = { version: APP_VERSION };
|
|
152
|
+
win.process = win.process || { env: {} };
|
|
153
|
+
win.process.env = win.process.env || {};
|
|
154
|
+
await loadScript(resolvePublicUrl(`${EXELEARNING_BASE_PATH}/importers.bundle.js`));
|
|
155
|
+
await loadScript(resolvePublicUrl(`${EXELEARNING_BASE_PATH}/exporters.bundle.js`));
|
|
156
|
+
if (!win.ElpxImporterCore || !win.createExporter) {
|
|
157
|
+
throw new Error('No se han podido cargar los bundles de eXeLearning.');
|
|
158
|
+
}
|
|
159
|
+
})();
|
|
160
|
+
return bundleLoadPromise;
|
|
161
|
+
}
|
|
162
|
+
function windowProxy() {
|
|
163
|
+
return window;
|
|
164
|
+
}
|
|
165
|
+
function resolvePublicUrl(path) {
|
|
166
|
+
const normalizedPath = path.replace(/^\/+/, '');
|
|
167
|
+
const baseUrl = import.meta.env?.BASE_URL ?? '/';
|
|
168
|
+
return new URL(normalizedPath, new URL(baseUrl, 'http://localhost/')).pathname;
|
|
169
|
+
}
|
|
170
|
+
function loadScript(src) {
|
|
171
|
+
if (isNodeLikeRuntime()) {
|
|
172
|
+
if (loadedScriptUrls.has(src)) {
|
|
173
|
+
return Promise.resolve();
|
|
174
|
+
}
|
|
175
|
+
return (async () => {
|
|
176
|
+
const response = await fetch(src);
|
|
177
|
+
if (!response.ok) {
|
|
178
|
+
throw new Error(`No se ha podido cargar ${src}`);
|
|
179
|
+
}
|
|
180
|
+
const code = await response.text();
|
|
181
|
+
globalThis.eval(`${code}\n//# sourceURL=${src}`);
|
|
182
|
+
loadedScriptUrls.add(src);
|
|
183
|
+
})();
|
|
184
|
+
}
|
|
185
|
+
const existing = document.querySelector(`script[data-exe-src="${src}"]`);
|
|
186
|
+
if (existing) {
|
|
187
|
+
if (existing.dataset.loaded === 'true') {
|
|
188
|
+
return Promise.resolve();
|
|
189
|
+
}
|
|
190
|
+
return new Promise((resolve, reject) => {
|
|
191
|
+
existing.addEventListener('load', () => resolve(), { once: true });
|
|
192
|
+
existing.addEventListener('error', () => reject(new Error(`No se ha podido cargar ${src}`)), { once: true });
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const script = document.createElement('script');
|
|
197
|
+
script.src = src;
|
|
198
|
+
script.async = false;
|
|
199
|
+
script.dataset.exeSrc = src;
|
|
200
|
+
script.addEventListener('load', () => {
|
|
201
|
+
script.dataset.loaded = 'true';
|
|
202
|
+
resolve();
|
|
203
|
+
}, { once: true });
|
|
204
|
+
script.addEventListener('error', () => reject(new Error(`No se ha podido cargar ${src}`)), { once: true });
|
|
205
|
+
document.head.append(script);
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
function isNodeLikeRuntime() {
|
|
209
|
+
return typeof process !== 'undefined' && !!process.versions?.node;
|
|
210
|
+
}
|
|
211
|
+
class MemoryDocumentManager {
|
|
212
|
+
ydoc;
|
|
213
|
+
projectId;
|
|
214
|
+
constructor(ydoc, projectId) {
|
|
215
|
+
this.ydoc = ydoc;
|
|
216
|
+
this.projectId = projectId || crypto.randomUUID();
|
|
217
|
+
}
|
|
218
|
+
getDoc() {
|
|
219
|
+
return this.ydoc;
|
|
220
|
+
}
|
|
221
|
+
getMetadata() {
|
|
222
|
+
return this.ydoc.getMap('metadata');
|
|
223
|
+
}
|
|
224
|
+
getNavigation() {
|
|
225
|
+
return this.ydoc.getArray('navigation');
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
class MemoryAssetStore {
|
|
229
|
+
projectId = crypto.randomUUID();
|
|
230
|
+
assets = new Map();
|
|
231
|
+
upsert(asset) {
|
|
232
|
+
this.assets.set(asset.id, asset);
|
|
233
|
+
}
|
|
234
|
+
async getProjectAssets() {
|
|
235
|
+
return Array.from(this.assets.values());
|
|
236
|
+
}
|
|
237
|
+
async getAsset(assetId) {
|
|
238
|
+
return this.assets.get(assetId) || null;
|
|
239
|
+
}
|
|
240
|
+
async getAllAssetsRaw() {
|
|
241
|
+
return Array.from(this.assets.values());
|
|
242
|
+
}
|
|
243
|
+
async resolveAssetURL(assetUrl) {
|
|
244
|
+
const assetId = parseAssetId(assetUrl);
|
|
245
|
+
if (!assetId) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
const asset = this.assets.get(assetId);
|
|
249
|
+
if (!asset) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
return URL.createObjectURL(asset.blob);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
class MemoryAssetHandler {
|
|
256
|
+
store;
|
|
257
|
+
constructor(store) {
|
|
258
|
+
this.store = store;
|
|
259
|
+
}
|
|
260
|
+
async storeAsset(id, data, metadata) {
|
|
261
|
+
const blob = new Blob([toBlobPart(data)], { type: metadata.mimeType || getMimeType(metadata.filename) });
|
|
262
|
+
const hash = await calculateHash(blob);
|
|
263
|
+
this.store.upsert({
|
|
264
|
+
id,
|
|
265
|
+
blob,
|
|
266
|
+
mime: metadata.mimeType || getMimeType(metadata.filename),
|
|
267
|
+
filename: metadata.filename,
|
|
268
|
+
originalPath: metadata.originalPath || metadata.filename,
|
|
269
|
+
folderPath: metadata.folderPath || '',
|
|
270
|
+
hash,
|
|
271
|
+
size: data.length,
|
|
272
|
+
projectId: this.store.projectId,
|
|
273
|
+
});
|
|
274
|
+
return id;
|
|
275
|
+
}
|
|
276
|
+
async extractAssetsFromZip(zip, onAssetProgress) {
|
|
277
|
+
const assetMap = new Map();
|
|
278
|
+
const assetFiles = [];
|
|
279
|
+
const isLegacyFormat = Object.keys(zip).some(path => path === 'contentv3.xml' || path.endsWith('/contentv3.xml'));
|
|
280
|
+
const uuidPattern = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
|
|
281
|
+
const customIdPattern = /^(idevice|block|page)-[a-z0-9]+-[a-z0-9]+$/i;
|
|
282
|
+
for (const [relativePath, fileData] of Object.entries(zip)) {
|
|
283
|
+
if (relativePath.endsWith('/')) {
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (relativePath.startsWith('__MACOSX')) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
if (relativePath.endsWith('.xml') || relativePath.endsWith('.xsd') || relativePath.endsWith('.data')) {
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
const isSystemFile = relativePath.startsWith('idevices/') ||
|
|
293
|
+
relativePath.startsWith('libs/') ||
|
|
294
|
+
relativePath.startsWith('theme/') ||
|
|
295
|
+
relativePath.startsWith('content/css/') ||
|
|
296
|
+
relativePath.startsWith('content/img/') ||
|
|
297
|
+
relativePath.startsWith('html/') ||
|
|
298
|
+
relativePath === 'index.html' ||
|
|
299
|
+
relativePath === 'base.css' ||
|
|
300
|
+
relativePath === 'common_i18n.js' ||
|
|
301
|
+
relativePath === 'common.js';
|
|
302
|
+
if (isSystemFile) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
let shouldInclude = false;
|
|
306
|
+
if (isLegacyFormat) {
|
|
307
|
+
shouldInclude = !relativePath.includes('/');
|
|
308
|
+
}
|
|
309
|
+
else {
|
|
310
|
+
const isResourceFile = relativePath.startsWith('resources/') ||
|
|
311
|
+
relativePath.startsWith('content/resources/') ||
|
|
312
|
+
relativePath.includes('/resources/');
|
|
313
|
+
if (isResourceFile) {
|
|
314
|
+
shouldInclude = true;
|
|
315
|
+
}
|
|
316
|
+
if (!shouldInclude) {
|
|
317
|
+
const pathParts = relativePath.split('/');
|
|
318
|
+
if (pathParts.length >= 2) {
|
|
319
|
+
const firstFolder = pathParts[0];
|
|
320
|
+
if (uuidPattern.test(firstFolder) || customIdPattern.test(firstFolder)) {
|
|
321
|
+
shouldInclude = true;
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (!shouldInclude) {
|
|
326
|
+
const isCustomFolderFile = relativePath.startsWith('custom/') && !relativePath.endsWith('/');
|
|
327
|
+
const customFilename = isCustomFolderFile ? relativePath.split('/').pop() : '';
|
|
328
|
+
const isCustomPlaceholder = Boolean(customFilename) && customFilename.startsWith('.');
|
|
329
|
+
if (isCustomFolderFile && !isCustomPlaceholder) {
|
|
330
|
+
shouldInclude = true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (shouldInclude) {
|
|
335
|
+
assetFiles.push({ path: relativePath, fileData });
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
for (let index = 0; index < assetFiles.length; index += 1) {
|
|
339
|
+
const { path, fileData } = assetFiles[index];
|
|
340
|
+
onAssetProgress?.(index + 1, assetFiles.length, path.split('/').pop() || path);
|
|
341
|
+
const mime = getMimeType(path);
|
|
342
|
+
const blob = new Blob([toBlobPart(fileData)], { type: mime });
|
|
343
|
+
const hash = await calculateHash(blob);
|
|
344
|
+
const assetId = hashToUUID(hash);
|
|
345
|
+
const filename = path.split('/').pop() || assetId;
|
|
346
|
+
const folderPath = extractFolderPathFromImport(path, assetId);
|
|
347
|
+
this.store.upsert({
|
|
348
|
+
id: assetId,
|
|
349
|
+
blob,
|
|
350
|
+
mime,
|
|
351
|
+
filename,
|
|
352
|
+
originalPath: path,
|
|
353
|
+
folderPath,
|
|
354
|
+
hash,
|
|
355
|
+
size: blob.size,
|
|
356
|
+
projectId: this.store.projectId,
|
|
357
|
+
});
|
|
358
|
+
assetMap.set(path, assetId);
|
|
359
|
+
if (path.startsWith('custom/') && path.includes(' ')) {
|
|
360
|
+
assetMap.set(path.replace(/ /g, '_'), assetId);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
return assetMap;
|
|
364
|
+
}
|
|
365
|
+
convertContextPathToAssetRefs(html, assetMap) {
|
|
366
|
+
if (!html) {
|
|
367
|
+
return html;
|
|
368
|
+
}
|
|
369
|
+
let convertedHtml = html;
|
|
370
|
+
const findAssetUrl = (assetPath) => {
|
|
371
|
+
const cleanPath = assetPath.replace(/[\\\s]+$/, '').trim();
|
|
372
|
+
if (assetMap.has(cleanPath)) {
|
|
373
|
+
return buildAssetUrl(assetMap.get(cleanPath), cleanPath.split('/').pop() || cleanPath);
|
|
374
|
+
}
|
|
375
|
+
for (const prefix of ['', 'content/', 'content/resources/', 'resources/']) {
|
|
376
|
+
const fullPath = `${prefix}${cleanPath}`;
|
|
377
|
+
if (assetMap.has(fullPath)) {
|
|
378
|
+
return buildAssetUrl(assetMap.get(fullPath), cleanPath.split('/').pop() || cleanPath);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const filename = cleanPath.split('/').pop() || cleanPath;
|
|
382
|
+
for (const [path, assetId] of assetMap.entries()) {
|
|
383
|
+
if (path.endsWith(`/${filename}`) || path === filename) {
|
|
384
|
+
return buildAssetUrl(assetId, filename);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const pathParts = cleanPath.split('/');
|
|
388
|
+
if (pathParts.length >= 2) {
|
|
389
|
+
const shortPath = pathParts.slice(-2).join('/');
|
|
390
|
+
for (const [path, assetId] of assetMap.entries()) {
|
|
391
|
+
if (path.endsWith(shortPath)) {
|
|
392
|
+
return buildAssetUrl(assetId, pathParts[pathParts.length - 1]);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const customPath = `custom/${filename}`;
|
|
397
|
+
if (assetMap.has(customPath)) {
|
|
398
|
+
return buildAssetUrl(assetMap.get(customPath), filename);
|
|
399
|
+
}
|
|
400
|
+
const denormalizedFilename = filename.replace(/_/g, ' ');
|
|
401
|
+
if (denormalizedFilename !== filename) {
|
|
402
|
+
const customDenormalized = `custom/${denormalizedFilename}`;
|
|
403
|
+
if (assetMap.has(customDenormalized)) {
|
|
404
|
+
return buildAssetUrl(assetMap.get(customDenormalized), denormalizedFilename);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
for (const [mapPath, assetId] of assetMap.entries()) {
|
|
408
|
+
if (!mapPath.startsWith('custom/')) {
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const mapFilename = mapPath.split('/').pop() || mapPath;
|
|
412
|
+
if (mapFilename === denormalizedFilename || mapFilename === filename) {
|
|
413
|
+
return buildAssetUrl(assetId, mapFilename);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
};
|
|
418
|
+
convertedHtml = convertedHtml.replace(/\{\{context_path\}\}\/([^"'<>]+)/g, (fullMatch, assetPath) => {
|
|
419
|
+
const url = findAssetUrl(assetPath);
|
|
420
|
+
return url || fullMatch;
|
|
421
|
+
});
|
|
422
|
+
convertedHtml = convertedHtml.replace(/(src|href)=(["'])resources\/([^"']+)\2/gi, (fullMatch, attr, quote, assetPath) => {
|
|
423
|
+
const url = findAssetUrl(`resources/${assetPath}`) || findAssetUrl(assetPath);
|
|
424
|
+
return url ? `${attr}=${quote}${url}${quote}` : fullMatch;
|
|
425
|
+
});
|
|
426
|
+
return convertedHtml;
|
|
427
|
+
}
|
|
428
|
+
async preloadAllAssets() { }
|
|
429
|
+
}
|
|
430
|
+
function decodeI18nString(value) {
|
|
431
|
+
return value.replace(/\\(["\\/bfnrt])/g, (_, ch) => {
|
|
432
|
+
switch (ch) {
|
|
433
|
+
case 'n':
|
|
434
|
+
return '\n';
|
|
435
|
+
case 't':
|
|
436
|
+
return '\t';
|
|
437
|
+
case 'r':
|
|
438
|
+
return '\r';
|
|
439
|
+
case 'b':
|
|
440
|
+
return '\b';
|
|
441
|
+
case 'f':
|
|
442
|
+
return '\f';
|
|
443
|
+
default:
|
|
444
|
+
return ch;
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
// Parse the i18n template (`"key": c_("English")`) into key -> English source.
|
|
449
|
+
function parseI18nTemplate(content) {
|
|
450
|
+
const map = new Map();
|
|
451
|
+
const re = /"([^"]+)"\s*:\s*c_\(\s*"((?:[^"\\]|\\.)*)"\s*\)/g;
|
|
452
|
+
let match;
|
|
453
|
+
while ((match = re.exec(content)) !== null) {
|
|
454
|
+
map.set(match[1], decodeI18nString(match[2]));
|
|
455
|
+
}
|
|
456
|
+
return map;
|
|
457
|
+
}
|
|
458
|
+
// Parse a resolved i18n file (`"key": "translation"`) into key -> translation.
|
|
459
|
+
function parseI18nResolved(content) {
|
|
460
|
+
const map = new Map();
|
|
461
|
+
const re = /"([^"]+)"\s*:\s*"((?:[^"\\]|\\.)*)"/g;
|
|
462
|
+
let match;
|
|
463
|
+
while ((match = re.exec(content)) !== null) {
|
|
464
|
+
map.set(match[1], decodeI18nString(match[2]));
|
|
465
|
+
}
|
|
466
|
+
return map;
|
|
467
|
+
}
|
|
468
|
+
class BundleResourceFetcher {
|
|
469
|
+
bundleCache = new Map();
|
|
470
|
+
ideviceCache = new Map();
|
|
471
|
+
libsBundlePromise = null;
|
|
472
|
+
commonBundlePromise = null;
|
|
473
|
+
async fetchTheme(themeName) {
|
|
474
|
+
return this.loadZipBundle(`${EXELEARNING_BASE_PATH}/bundles/themes/${themeName}.zip`);
|
|
475
|
+
}
|
|
476
|
+
async fetchIdevice(ideviceType) {
|
|
477
|
+
const normalized = normalizeIdeviceType(ideviceType);
|
|
478
|
+
if (!this.ideviceCache.has(normalized)) {
|
|
479
|
+
const allFiles = await this.loadZipBundle(`${EXELEARNING_BASE_PATH}/bundles/idevices.zip`);
|
|
480
|
+
const grouped = new Map();
|
|
481
|
+
for (const [path, blob] of allFiles.entries()) {
|
|
482
|
+
const parts = path.split('/');
|
|
483
|
+
if (parts.length < 2) {
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
const bucket = grouped.get(parts[0]) || new Map();
|
|
487
|
+
bucket.set(parts.slice(1).join('/'), blob);
|
|
488
|
+
grouped.set(parts[0], bucket);
|
|
489
|
+
}
|
|
490
|
+
for (const [key, files] of grouped.entries()) {
|
|
491
|
+
this.ideviceCache.set(key, files);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
return this.ideviceCache.get(normalized) || new Map();
|
|
495
|
+
}
|
|
496
|
+
async fetchBaseLibraries() {
|
|
497
|
+
if (!this.libsBundlePromise) {
|
|
498
|
+
this.libsBundlePromise = this.loadZipBundle(`${EXELEARNING_BASE_PATH}/bundles/libs.zip`);
|
|
499
|
+
}
|
|
500
|
+
return new Map(await this.libsBundlePromise);
|
|
501
|
+
}
|
|
502
|
+
async fetchScormFiles() {
|
|
503
|
+
return new Map();
|
|
504
|
+
}
|
|
505
|
+
async fetchLibraryFiles(paths) {
|
|
506
|
+
const result = new Map();
|
|
507
|
+
const [libs, common] = await Promise.all([this.fetchBaseLibraries(), this.fetchCommonBundle()]);
|
|
508
|
+
for (const path of paths) {
|
|
509
|
+
const file = libs.get(path) || common.get(path);
|
|
510
|
+
if (file) {
|
|
511
|
+
result.set(path, file);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
async fetchLibraryDirectory(libraryName) {
|
|
517
|
+
const result = new Map();
|
|
518
|
+
const [libs, common] = await Promise.all([this.fetchBaseLibraries(), this.fetchCommonBundle()]);
|
|
519
|
+
const prefix = `${libraryName}/`;
|
|
520
|
+
for (const [path, blob] of libs.entries()) {
|
|
521
|
+
if (path.startsWith(prefix)) {
|
|
522
|
+
result.set(path, blob);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
for (const [path, blob] of common.entries()) {
|
|
526
|
+
if (path.startsWith(prefix)) {
|
|
527
|
+
result.set(path, blob);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
return result;
|
|
531
|
+
}
|
|
532
|
+
async fetchExeLogo() {
|
|
533
|
+
const response = await fetch(resolvePublicUrl(`${EXELEARNING_BASE_PATH}/app/common/exe_powered_logo/exe_powered_logo.png`));
|
|
534
|
+
if (!response.ok) {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
return response.blob();
|
|
538
|
+
}
|
|
539
|
+
async fetchContentCss() {
|
|
540
|
+
return this.loadZipBundle(`${EXELEARNING_BASE_PATH}/bundles/content-css.zip`);
|
|
541
|
+
}
|
|
542
|
+
// Returns the pre-built common_i18n.{lang}.js for the given language (key ->
|
|
543
|
+
// translation, with c_() already resolved), or null when unavailable.
|
|
544
|
+
async fetchI18nFile(language) {
|
|
545
|
+
return this.loadI18nScript(language);
|
|
546
|
+
}
|
|
547
|
+
// Returns a source(English) -> target map, the way eXeLearning derives it in
|
|
548
|
+
// static mode: by combining the template common_i18n.js (key -> c_("English"))
|
|
549
|
+
// with the resolved common_i18n.{lang}.js (key -> translation).
|
|
550
|
+
async fetchI18nTranslations(language) {
|
|
551
|
+
const [templateText, resolvedText] = await Promise.all([
|
|
552
|
+
this.loadI18nTemplate(),
|
|
553
|
+
this.loadI18nScript(language),
|
|
554
|
+
]);
|
|
555
|
+
if (!templateText || !resolvedText) {
|
|
556
|
+
return {};
|
|
557
|
+
}
|
|
558
|
+
const sources = parseI18nTemplate(templateText);
|
|
559
|
+
const targets = parseI18nResolved(resolvedText);
|
|
560
|
+
const record = {};
|
|
561
|
+
for (const [key, source] of sources) {
|
|
562
|
+
const target = targets.get(key);
|
|
563
|
+
if (source && target !== undefined) {
|
|
564
|
+
record[source] = target;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
return record;
|
|
568
|
+
}
|
|
569
|
+
async loadI18nScript(language) {
|
|
570
|
+
const candidates = [language];
|
|
571
|
+
const baseLang = language.split('-')[0];
|
|
572
|
+
if (baseLang && baseLang !== language) {
|
|
573
|
+
candidates.push(baseLang);
|
|
574
|
+
}
|
|
575
|
+
for (const lang of candidates) {
|
|
576
|
+
const text = await this.fetchTextFile(`${EXELEARNING_BASE_PATH}/app/common/i18n/common_i18n.${lang}.js`);
|
|
577
|
+
if (text !== null) {
|
|
578
|
+
return text;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
loadI18nTemplate() {
|
|
584
|
+
return this.fetchTextFile(`${EXELEARNING_BASE_PATH}/app/common/common_i18n.js`);
|
|
585
|
+
}
|
|
586
|
+
async fetchTextFile(relativePath) {
|
|
587
|
+
const response = await fetch(resolvePublicUrl(relativePath));
|
|
588
|
+
if (!response.ok) {
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
591
|
+
return response.text();
|
|
592
|
+
}
|
|
593
|
+
async fetchGlobalFontFiles() {
|
|
594
|
+
return new Map();
|
|
595
|
+
}
|
|
596
|
+
async fetchCommonBundle() {
|
|
597
|
+
if (!this.commonBundlePromise) {
|
|
598
|
+
this.commonBundlePromise = this.loadZipBundle(`${EXELEARNING_BASE_PATH}/bundles/common.zip`);
|
|
599
|
+
}
|
|
600
|
+
return new Map(await this.commonBundlePromise);
|
|
601
|
+
}
|
|
602
|
+
loadZipBundle(relativePath) {
|
|
603
|
+
const url = resolvePublicUrl(relativePath);
|
|
604
|
+
const cached = this.bundleCache.get(url);
|
|
605
|
+
if (cached) {
|
|
606
|
+
return cached;
|
|
607
|
+
}
|
|
608
|
+
const promise = (async () => {
|
|
609
|
+
const response = await fetch(url);
|
|
610
|
+
if (!response.ok) {
|
|
611
|
+
return new Map();
|
|
612
|
+
}
|
|
613
|
+
const bytes = new Uint8Array(await response.arrayBuffer());
|
|
614
|
+
const entries = unzipSync(bytes);
|
|
615
|
+
const files = new Map();
|
|
616
|
+
for (const [path, data] of Object.entries(entries)) {
|
|
617
|
+
files.set(path, new Blob([toBlobPart(data)], { type: getMimeType(path) }));
|
|
618
|
+
}
|
|
619
|
+
return files;
|
|
620
|
+
})();
|
|
621
|
+
this.bundleCache.set(url, promise);
|
|
622
|
+
return promise;
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function toElpxFilename(filename) {
|
|
626
|
+
return filename.replace(/\.[^.]+$/u, '') + '.elpx';
|
|
627
|
+
}
|
|
628
|
+
function getMimeType(filename) {
|
|
629
|
+
const extension = filename.split('.').pop()?.toLowerCase() || '';
|
|
630
|
+
return MIME_TYPES[extension] || 'application/octet-stream';
|
|
631
|
+
}
|
|
632
|
+
async function calculateHash(blob) {
|
|
633
|
+
const arrayBuffer = await blob.arrayBuffer();
|
|
634
|
+
if (crypto.subtle?.digest) {
|
|
635
|
+
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
|
|
636
|
+
return Array.from(new Uint8Array(hashBuffer))
|
|
637
|
+
.map(value => value.toString(16).padStart(2, '0'))
|
|
638
|
+
.join('');
|
|
639
|
+
}
|
|
640
|
+
const data = new Uint8Array(arrayBuffer);
|
|
641
|
+
let hash = 2166136261;
|
|
642
|
+
for (let index = 0; index < data.length; index += 1) {
|
|
643
|
+
hash ^= data[index];
|
|
644
|
+
hash = (hash * 16777619) >>> 0;
|
|
645
|
+
}
|
|
646
|
+
const sizeHash = (data.length * 2654435761) >>> 0;
|
|
647
|
+
const sample1 = data.length > 0 ? data[0] : 0;
|
|
648
|
+
const sample2 = data.length > 100 ? data[100] : 0;
|
|
649
|
+
const sample3 = data.length > 1000 ? data[1000] : 0;
|
|
650
|
+
return [
|
|
651
|
+
hash.toString(16).padStart(8, '0'),
|
|
652
|
+
sizeHash.toString(16).padStart(8, '0'),
|
|
653
|
+
(hash ^ sizeHash).toString(16).padStart(8, '0'),
|
|
654
|
+
((hash + sample1 + sample2 + sample3) >>> 0).toString(16).padStart(8, '0'),
|
|
655
|
+
data.length.toString(16).padStart(8, '0'),
|
|
656
|
+
((hash * 31 + sizeHash) >>> 0).toString(16).padStart(8, '0'),
|
|
657
|
+
((sizeHash ^ sample1 ^ sample2 ^ sample3) >>> 0).toString(16).padStart(8, '0'),
|
|
658
|
+
((hash ^ data.length) >>> 0).toString(16).padStart(8, '0'),
|
|
659
|
+
].join('');
|
|
660
|
+
}
|
|
661
|
+
function hashToUUID(hash) {
|
|
662
|
+
const value = hash.substring(0, 32);
|
|
663
|
+
return `${value.substring(0, 8)}-${value.substring(8, 12)}-${value.substring(12, 16)}-${value.substring(16, 20)}-${value.substring(20, 32)}`;
|
|
664
|
+
}
|
|
665
|
+
function buildAssetUrl(assetId, filename) {
|
|
666
|
+
const extension = filename.includes('.') ? filename.split('.').pop()?.toLowerCase() : '';
|
|
667
|
+
return extension ? `asset://${assetId}.${extension}` : `asset://${assetId}`;
|
|
668
|
+
}
|
|
669
|
+
function toBlobPart(data) {
|
|
670
|
+
return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength);
|
|
671
|
+
}
|
|
672
|
+
function parseAssetId(assetUrl) {
|
|
673
|
+
if (!assetUrl.startsWith('asset://')) {
|
|
674
|
+
return null;
|
|
675
|
+
}
|
|
676
|
+
const value = assetUrl.slice('asset://'.length);
|
|
677
|
+
return value.split('.')[0] || null;
|
|
678
|
+
}
|
|
679
|
+
function extractFolderPathFromImport(path, assetId) {
|
|
680
|
+
let cleanPath = path;
|
|
681
|
+
if (cleanPath.startsWith('content/resources/')) {
|
|
682
|
+
cleanPath = cleanPath.slice('content/resources/'.length);
|
|
683
|
+
}
|
|
684
|
+
else if (cleanPath.startsWith('resources/')) {
|
|
685
|
+
cleanPath = cleanPath.slice('resources/'.length);
|
|
686
|
+
}
|
|
687
|
+
const parts = cleanPath.split('/');
|
|
688
|
+
parts.pop();
|
|
689
|
+
if (parts.length === 0) {
|
|
690
|
+
return '';
|
|
691
|
+
}
|
|
692
|
+
const firstPart = parts[0];
|
|
693
|
+
const isUuidLike = /^[a-f0-9-]{8,}$/i.test(firstPart);
|
|
694
|
+
if (isUuidLike && parts.length === 1) {
|
|
695
|
+
if (firstPart === assetId || firstPart.startsWith(assetId.split('-')[0])) {
|
|
696
|
+
return '';
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (isUuidLike && parts.length > 1) {
|
|
700
|
+
return parts.slice(1).join('/');
|
|
701
|
+
}
|
|
702
|
+
return parts.join('/');
|
|
703
|
+
}
|
|
704
|
+
function normalizeIdeviceType(typeName) {
|
|
705
|
+
if (!typeName) {
|
|
706
|
+
return 'text';
|
|
707
|
+
}
|
|
708
|
+
const normalized = typeName.toLowerCase().replace(/-?idevice$/i, '');
|
|
709
|
+
return IDEVICE_TYPE_MAP[normalized] || normalized || 'text';
|
|
710
|
+
}
|
|
711
|
+
function buildPreviewPagesFromElpx(elpxData) {
|
|
712
|
+
revokePreviewObjectUrls();
|
|
713
|
+
const entries = unzipSync(elpxData);
|
|
714
|
+
const htmlEntries = new Set(Object.keys(entries).filter(path => path === 'index.html' || path.startsWith('html/')));
|
|
715
|
+
const blobUrls = new Map();
|
|
716
|
+
for (const [path, data] of Object.entries(entries)) {
|
|
717
|
+
if (htmlEntries.has(path) || path.endsWith('/') || path === 'content.xml' || path === 'content.dtd') {
|
|
718
|
+
continue;
|
|
719
|
+
}
|
|
720
|
+
const url = URL.createObjectURL(new Blob([toBlobPart(data)], { type: getMimeType(path) }));
|
|
721
|
+
blobUrls.set(path, url);
|
|
722
|
+
previewObjectUrls.push(url);
|
|
723
|
+
}
|
|
724
|
+
for (const [path, data] of Object.entries(entries)) {
|
|
725
|
+
if (htmlEntries.has(path) || !path.toLowerCase().endsWith('.css')) {
|
|
726
|
+
continue;
|
|
727
|
+
}
|
|
728
|
+
const rewrittenCss = rewriteCssUrls(new TextDecoder().decode(data), path, blobUrls);
|
|
729
|
+
const oldUrl = blobUrls.get(path);
|
|
730
|
+
if (oldUrl) {
|
|
731
|
+
URL.revokeObjectURL(oldUrl);
|
|
732
|
+
previewObjectUrls = previewObjectUrls.filter(url => url !== oldUrl);
|
|
733
|
+
}
|
|
734
|
+
const cssUrl = URL.createObjectURL(new Blob([rewrittenCss], { type: 'text/css' }));
|
|
735
|
+
blobUrls.set(path, cssUrl);
|
|
736
|
+
previewObjectUrls.push(cssUrl);
|
|
737
|
+
}
|
|
738
|
+
const pages = {};
|
|
739
|
+
for (const htmlPath of htmlEntries) {
|
|
740
|
+
const source = new TextDecoder().decode(entries[htmlPath]);
|
|
741
|
+
pages[htmlPath] = rewritePreviewHtml(source, htmlPath, entries, htmlEntries, blobUrls);
|
|
742
|
+
}
|
|
743
|
+
return pages;
|
|
744
|
+
}
|
|
745
|
+
function revokePreviewObjectUrls() {
|
|
746
|
+
for (const url of previewObjectUrls) {
|
|
747
|
+
URL.revokeObjectURL(url);
|
|
748
|
+
}
|
|
749
|
+
previewObjectUrls = [];
|
|
750
|
+
}
|
|
751
|
+
function rewritePreviewHtml(html, currentPath, entries, htmlEntries, blobUrls) {
|
|
752
|
+
const document = new DOMParser().parseFromString(html, 'text/html');
|
|
753
|
+
for (const link of Array.from(document.querySelectorAll('link[rel="stylesheet"][href]'))) {
|
|
754
|
+
const href = link.getAttribute('href');
|
|
755
|
+
if (!href) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
const resolved = resolveZipPath(currentPath, href);
|
|
759
|
+
if (!resolved) {
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
const blobUrl = blobUrls.get(resolved);
|
|
763
|
+
if (blobUrl) {
|
|
764
|
+
link.setAttribute('href', blobUrl);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
for (const script of Array.from(document.querySelectorAll('script[src]'))) {
|
|
768
|
+
const src = script.getAttribute('src');
|
|
769
|
+
if (!src) {
|
|
770
|
+
continue;
|
|
771
|
+
}
|
|
772
|
+
const resolved = resolveZipPath(currentPath, src);
|
|
773
|
+
if (!resolved) {
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
const blobUrl = blobUrls.get(resolved);
|
|
777
|
+
if (blobUrl) {
|
|
778
|
+
script.setAttribute('src', blobUrl);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
const attributes = ['src', 'href', 'poster', 'data'];
|
|
782
|
+
for (const attribute of attributes) {
|
|
783
|
+
for (const element of Array.from(document.querySelectorAll(`[${attribute}]`))) {
|
|
784
|
+
const value = element.getAttribute(attribute);
|
|
785
|
+
if (!value || isExternalUrl(value) || value.startsWith('#')) {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
const resolved = resolveZipPath(currentPath, value);
|
|
789
|
+
if (!resolved) {
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
if (attribute === 'href' && htmlEntries.has(resolved)) {
|
|
793
|
+
continue;
|
|
794
|
+
}
|
|
795
|
+
const blobUrl = blobUrls.get(resolved);
|
|
796
|
+
if (blobUrl) {
|
|
797
|
+
element.setAttribute(attribute, blobUrl);
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
return '<!doctype html>\n' + document.documentElement.outerHTML;
|
|
802
|
+
}
|
|
803
|
+
function rewriteCssUrls(css, cssPath, blobUrls) {
|
|
804
|
+
return css.replace(/url\(([^)]+)\)/g, (fullMatch, rawValue) => {
|
|
805
|
+
const unquoted = rawValue.trim().replace(/^['"]|['"]$/g, '');
|
|
806
|
+
if (!unquoted || isExternalUrl(unquoted) || unquoted.startsWith('data:')) {
|
|
807
|
+
return fullMatch;
|
|
808
|
+
}
|
|
809
|
+
const resolved = resolveZipPath(cssPath, unquoted);
|
|
810
|
+
if (!resolved) {
|
|
811
|
+
return fullMatch;
|
|
812
|
+
}
|
|
813
|
+
const blobUrl = blobUrls.get(resolved);
|
|
814
|
+
if (!blobUrl) {
|
|
815
|
+
return fullMatch;
|
|
816
|
+
}
|
|
817
|
+
return `url("${blobUrl}")`;
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
function resolveZipPath(fromPath, targetPath) {
|
|
821
|
+
if (!targetPath || isExternalUrl(targetPath)) {
|
|
822
|
+
return null;
|
|
823
|
+
}
|
|
824
|
+
const cleanTarget = targetPath.split('#')[0].split('?')[0];
|
|
825
|
+
if (!cleanTarget) {
|
|
826
|
+
return null;
|
|
827
|
+
}
|
|
828
|
+
const segments = fromPath.split('/');
|
|
829
|
+
segments.pop();
|
|
830
|
+
const parts = cleanTarget.split('/');
|
|
831
|
+
const output = cleanTarget.startsWith('/') ? [] : segments;
|
|
832
|
+
for (const part of parts) {
|
|
833
|
+
if (!part || part === '.') {
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
if (part === '..') {
|
|
837
|
+
output.pop();
|
|
838
|
+
continue;
|
|
839
|
+
}
|
|
840
|
+
output.push(part);
|
|
841
|
+
}
|
|
842
|
+
return output.join('/');
|
|
843
|
+
}
|
|
844
|
+
function isExternalUrl(value) {
|
|
845
|
+
return /^(?:[a-z]+:)?\/\//i.test(value) || value.startsWith('blob:') || value.startsWith('data:') || value.startsWith('mailto:');
|
|
846
|
+
}
|