| 1 | import fs from 'node:fs'; |
| 2 | import path from 'node:path'; |
| 3 | import JSZip from 'jszip'; |
| 4 | import { globSync } from 'glob'; |
| 5 | |
| 6 | function switchToStaticScripts(htmlContent) { |
| 7 | // Look for the module script block and capture indentation |
| 8 | const moduleScriptPattern = /(\s*)<script type="module">([\s\S]*?)<\/script>/; |
| 9 | const match = htmlContent.match(moduleScriptPattern); |
| 10 | |
| 11 | if (!match) return htmlContent; |
| 12 | |
| 13 | const indentation = match[1].replace(/\n/g, ''); |
| 14 | let moduleCode = match[2]; |
| 15 | const scriptPaths = []; |
| 16 | const pluginAliasMap = new Map(); |
| 17 | const moduleAliasMap = new Map(); |
| 18 | |
| 19 | const addScriptPath = (scriptPath) => { |
| 20 | if (!scriptPaths.includes(scriptPath)) { |
| 21 | scriptPaths.push(scriptPath); |
| 22 | } |
| 23 | }; |
| 24 | |
| 25 | const replaceIdentifier = (code, identifier, replacement) => { |
| 26 | if (!identifier || identifier === replacement) return code; |
| 27 | const escaped = identifier.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| 28 | return code.replace(new RegExp(`\\b${escaped}\\b`, 'g'), replacement); |
| 29 | }; |
| 30 | |
| 31 | // Replace main reveal.js import. |
| 32 | moduleCode = moduleCode.replace( |
| 33 | /^\s*import\s+(\w+)\s+from\s+['"]reveal\.js['"]\s*;?\s*$/gm, |
| 34 | (match, revealVar) => { |
| 35 | addScriptPath('dist/reveal.js'); |
| 36 | moduleAliasMap.set(revealVar, 'Reveal'); |
| 37 | return ''; |
| 38 | } |
| 39 | ); |
| 40 | |
| 41 | // Replace plugin imports |
| 42 | moduleCode = moduleCode.replace( |
| 43 | /^\s*import\s+(\w+)\s+from\s+['"]reveal\.js\/plugin\/(\w+)['"]\s*;?\s*$/gm, |
| 44 | (match, pluginVar, pluginName) => { |
| 45 | const pluginGlobal = `Reveal${pluginName.charAt(0).toUpperCase()}${pluginName.slice(1)}`; |
| 46 | addScriptPath(`dist/plugin/${pluginName}.js`); |
| 47 | pluginAliasMap.set(pluginVar, pluginGlobal); |
| 48 | return ''; |
| 49 | } |
| 50 | ); |
| 51 | |
| 52 | for (const [pluginVar, pluginGlobal] of pluginAliasMap) { |
| 53 | moduleCode = replaceIdentifier(moduleCode, pluginVar, pluginGlobal); |
| 54 | } |
| 55 | for (const [moduleVar, moduleGlobal] of moduleAliasMap) { |
| 56 | moduleCode = replaceIdentifier(moduleCode, moduleVar, moduleGlobal); |
| 57 | } |
| 58 | |
| 59 | // Clean up any remaining empty lines and trim |
| 60 | moduleCode = moduleCode.replace(/^\s*[\r\n]/gm, '').trim(); |
| 61 | |
| 62 | const scriptTags = scriptPaths.map((scriptPath) => `${indentation}<script src="${scriptPath}"></script>`); |
| 63 | const replacement = |
| 64 | '\n\n' + |
| 65 | scriptTags.join('\n') + |
| 66 | `\n${indentation}<script>\n${indentation}\t${moduleCode}\n${indentation}</script>`; |
| 67 | |
| 68 | return htmlContent.replace(moduleScriptPattern, replacement); |
| 69 | } |
| 70 | |
| 71 | /** |
| 72 | * Replace paths to dynamic CSS/SCSS files with static ones. |
| 73 | */ |
| 74 | function switchToStaticStyles(htmlContent) { |
| 75 | // Replace /css/* links with /dist/* |
| 76 | htmlContent = htmlContent.replace(/href="css\/([^"]+\.(css|scss))"/g, (match, filePath) => { |
| 77 | const cssPath = filePath.replace(/\.scss$/, '.css'); |
| 78 | return `href="dist/${cssPath}"`; |
| 79 | }); |
| 80 | |
| 81 | // Replace /plugin/* links with /dist/plugin/* |
| 82 | htmlContent = htmlContent.replace(/href="plugin\/([^"]+\.(css|scss))"/g, (match, filePath) => { |
| 83 | const cssPath = filePath.replace(/\.scss$/, '.css'); |
| 84 | return `href="dist/plugin/${cssPath}"`; |
| 85 | }); |
| 86 | |
| 87 | return htmlContent; |
| 88 | } |
| 89 | |
| 90 | async function main() { |
| 91 | // Parse command line arguments for HTML file target |
| 92 | const args = process.argv.slice(2); |
| 93 | const htmlTarget = args.length > 0 ? args[0] : 'index.html'; |
| 94 | |
| 95 | // Ensure relative paths are read from cwd while keeping absolute paths intact |
| 96 | const targetFile = path.isAbsolute(htmlTarget) |
| 97 | ? htmlTarget |
| 98 | : htmlTarget.startsWith('./') |
| 99 | ? htmlTarget |
| 100 | : `./${htmlTarget}`; |
| 101 | |
| 102 | console.log(`Packaging presentation with target file: ${targetFile}`); |
| 103 | |
| 104 | // Read the HTML file |
| 105 | let htmlContent = fs.readFileSync(targetFile, 'utf8'); |
| 106 | |
| 107 | // Switch from Vite's dynamic imports to static ones so that |
| 108 | // this presentation can run anywhere (including offline via |
| 109 | // file:// protocol) |
| 110 | htmlContent = switchToStaticScripts(htmlContent); |
| 111 | htmlContent = switchToStaticStyles(htmlContent); |
| 112 | |
| 113 | const zip = new JSZip(); |
| 114 | const filesToInclude = ['./dist/**', './*/*.md']; |
| 115 | |
| 116 | if (fs.existsSync('./lib')) filesToInclude.push('./lib/**'); |
| 117 | if (fs.existsSync('./images')) filesToInclude.push('./images/**'); |
| 118 | if (fs.existsSync('./slides')) filesToInclude.push('./slides/**'); |
| 119 | |
| 120 | // Add the modified HTML file first |
| 121 | const htmlFileName = htmlTarget.replace(/\.\//, ''); |
| 122 | zip.file(htmlFileName, htmlContent); |
| 123 | |
| 124 | for (const pattern of filesToInclude) { |
| 125 | const files = globSync(pattern, { |
| 126 | nodir: true, |
| 127 | dot: false, |
| 128 | ignore: ['./examples/**', './test/**'], |
| 129 | }); |
| 130 | for (const file of files) { |
| 131 | const filePath = path.resolve(file); |
| 132 | const relativePath = path.relative(process.cwd(), filePath); |
| 133 | const fileData = fs.readFileSync(filePath); |
| 134 | zip.file(relativePath, fileData); |
| 135 | } |
| 136 | } |
| 137 | |
| 138 | const content = await zip.generateAsync({ type: 'nodebuffer' }); |
| 139 | const zipFileName = `presentation.zip`; |
| 140 | fs.writeFileSync(zipFileName, content); |
| 141 | console.log(`Presentation packaged successfully: ${zipFileName}`); |
| 142 | } |
| 143 | |
| 144 | main().catch((error) => { |
| 145 | console.error('Error packaging presentation:', error); |
| 146 | process.exit(1); |
| 147 | }); |
| 148 |