diff --git a/.github/workflows/test-and-deploy.yml b/.github/workflows/test-and-deploy.yml index 99e7fbc7..f85cbe03 100644 --- a/.github/workflows/test-and-deploy.yml +++ b/.github/workflows/test-and-deploy.yml @@ -45,7 +45,6 @@ jobs: run: | ./build.js ./tools/package-inform7.sh - ./tools/make-single-file.js - name: Test storyfiles run: ./tests/runtests.sh - name: Check browser compatibility diff --git a/README.md b/README.md index 15bcd56b..a683028b 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ python3 ifsitegen.py -i parchment-for-inform7.zip Storyfile.ulx Single File Build ----------------- -Parchment is also available as a single file, suitable for downloading and using offline. [Download it here](https://github.com/curiousdannii/parchment/raw/gh-pages/dist/single-file/parchment-single-file.zip). +Parchment is also available as a single file, suitable for downloading and using offline. [Download from the releases page](https://github.com/curiousdannii/parchment/releases). Free Software ------------- diff --git a/src/tools/single-file.js b/src/tools/single-file.js new file mode 100644 index 00000000..714a2aac --- /dev/null +++ b/src/tools/single-file.js @@ -0,0 +1,99 @@ +/* + +Common single-file processing +============================= + +Copyright (c) 2023 Dannii Willis +MIT licenced +https://github.com/curiousdannii/parchment + +*/ + +const utf8decoder = new TextDecoder() + +async function Uint8Array_to_base64(data) { + if (typeof Buffer !== 'undefined') { + return data.toString('base64') + } + // From https://stackoverflow.com/a/66046176/2854284 + else if (typeof FileReader !== 'undefined') { + return (await new Promise(resolve => { + const reader = new FileReader() + reader.onload = () => resolve(reader.result) + reader.readAsDataURL(new Blob([data])) + })).split(',', 2)[1] + } +} + +export async function generate(options, files) { + const inclusions = [] + if (options.single_file) { + inclusions.push('') + } + + // Process the files + for (const filename of Object.keys(files)) { + if (/\.(css|js|html)$/.test(filename)) { + files[filename] = utf8decoder.decode(files[filename]) + .replace(/(\/\/|\/\*)# sourceMappingURL.+/, '') + .trim() + } + let data = files[filename] + if (filename === 'ie.js') { + inclusions.push(``) + } + else if (filename === 'jquery.min.js') { + inclusions.push(``) + } + else if (filename === 'main.js') { + inclusions.push(``) + } + else if (filename.endsWith('.css')) { + // Only include a single font, the browser can fake bold and italics + const fontfile = await Uint8Array_to_base64(files['iosevka-extended.woff2']) + data = data.replace(/@font-face{font-family:([' \w]+);font-style:(\w+);font-weight:(\d+);src:url\([^)]+\) format\(['"]woff2['"]\)}/g, (_, font, style, weight) => { + if (font === 'Iosevka' && style === 'normal' && weight === '400' && options.font) { + return `@font-face{font-family:Iosevka;font-style:normal;font-weight:400;src:url(data:font/woff2;base64,${fontfile}) format('woff2')}` + } + return '' + }) + .replace(/Iosevka Narrow/g, 'Iosevka') + if (!options.font) { + data = data.replace(/--glkote(-grid)?-mono-family: "Iosevka", monospace;?/g, '') + } + inclusions.push(``) + } + else if (filename.endsWith('.js')) { + inclusions.push(``) + } + else if (filename.endsWith('.wasm')) { + inclusions.push(``) + } + } + + // Inject into index.html + let indexhtml = files['index.html'] + const gif = await Uint8Array_to_base64(files['waiting.gif']) + indexhtml = indexhtml + .replace(//g, '') + .replace(//g, '') + .replace(//, `${title}`) + .replace(/<\/noscript>/, `\n`) + } + + // Add the inclusions + const parts = indexhtml.split(/\s*<\/head>/) + indexhtml = `${parts[0]} + ${inclusions.join('\n')} + ${parts[1]}` + + return indexhtml +} \ No newline at end of file diff --git a/tools/make-single-file.js b/tools/make-single-file.js index 4de8f26d..22ed402e 100755 --- a/tools/make-single-file.js +++ b/tools/make-single-file.js @@ -4,144 +4,95 @@ Parchment single-file converter =============================== -Copyright (c) 2022 Dannii Willis +Copyright (c) 2023 Dannii Willis MIT licenced https://github.com/curiousdannii/parchment */ import child_process from 'child_process' -import crypto from 'crypto' import fs from 'fs/promises' -import fs_sync from 'fs' import path from 'path' import {fileURLToPath} from 'url' import util from 'util' -import minimist from 'minimist' +import {generate} from '../src/tools/single-file.js' const execFile = util.promisify(child_process.execFile) -// Handle command line arguments -const argv = minimist(process.argv.slice(2)) -const datemode = argv.date -const exclude = argv.exclude || 'bocfel|git|glulx' -const excluded = new RegExp(`(${exclude})`) -const nofont = argv.nofont - const rootpath = path.join(path.dirname(fileURLToPath(import.meta.url)), '..') const webpath = path.join(rootpath, 'dist/web') -// Get all the files -const filenames = await fs.readdir(webpath) - -async function file_to_base64(path) { - return (await fs.readFile(path)).toString('base64') +// Presets and options +const base_options = { + date: 1, + font: 1, + single_file: 1, + terps: [], +} +const presets = { + dist: { + terps: ['hugo', 'quixe', 'scare', 'tads', 'zvm'], + }, + frankendrift: { + single_file: 0, + terps: [], + }, + regtest: { + font: 0, + terps: ['quixe', 'zvm'], + }, } -// Turn the filenames into embeddable resources -const files = await Promise.all(filenames.map(async filename => { - // Skip unused interpreters - if (excluded.test(filename)) { - return - } - let data = await fs.readFile(path.join(webpath, filename), {encoding: filename.endsWith('.wasm') ? null : 'utf8'}) - if (!filename.endsWith('.wasm')) { - data = data.replace(/(\/\/|\/\*)# sourceMappingURL.+/, '') - .trim() - } - if (filename === 'ie.js') { - return `` - } - if (filename === 'jquery.min.js') { - return `` - } - if (filename === 'main.js') { - return `` - } - if (filename.endsWith('.css')) { - // Only include a single font, the browser can fake bold and italics - const fontfile = await file_to_base64(path.join(webpath, '../fonts/iosevka/iosevka-extended.woff2')) - data = data.replace(/@font-face{font-family:([' \w]+);font-style:(\w+);font-weight:(\d+);src:url\([^)]+\) format\(['"]woff2['"]\)}/g, (_, font, style, weight) => { - if (font === 'Iosevka' && style === 'normal' && weight === '400' && !nofont) { - return `@font-face{font-family:Iosevka;font-style:normal;font-weight:400;src:url(data:font/woff2;base64,${fontfile}) format('woff2')}` - } - return '' - }) - .replace(/Iosevka Narrow/g, 'Iosevka') - if (nofont) { - data = data.replace(/--glkote(-grid)?-mono-family: "Iosevka", monospace;?/g, '') - } - return `` - } - if (filename.endsWith('.js')) { - return `` - } - if (filename.endsWith('.wasm')) { - return `` - } - return -})) - -// Inject into index.html -let indexhtml = await fs.readFile(path.join(rootpath, 'index.html'), {encoding: 'utf8'}) -indexhtml = indexhtml - .replace(//g, '') - .replace(//g, '') - .replace(//, `${title}`) - .replace(/<\/noscript>/, `\n`) +// Get all the files +const files = {} +for (const file of common.concat(options.terps.map(terp => terps[terp]).flat())) { + files[path.basename(file)] = await fs.readFile(path.join(webpath, file)) } -const parts = indexhtml.split(/\s*<\/head>/) -indexhtml = `${parts[0]} - -${files.filter(file => file).join('\n')} -${parts[1]}` +const indexhtml = await generate(options, files) const outdir = path.join(webpath, '../single-file') await fs.mkdir(outdir, {recursive: true}) const outpath = path.join(outdir, 'parchment.html') -// Download the existing published file if necessary -if (!fs_sync.existsSync(outpath)) { - console.log('Downloading old dist/single-file/parchment-single-file.zip') - const zippath = path.join(outdir, 'parchment-single-file.zip') - const branch = (await execFile('git', ['rev-parse', '--abbrev-ref', 'HEAD'])).stdout.trim() - await execFile('curl', ['-L', '-o', zippath, `https://github.com/curiousdannii/parchment${branch === 'testing' ? '-testing' : ''}/raw/gh-pages/dist/single-file/parchment-single-file.zip`]) - await execFile('unzip', [zippath, '-d', outdir]) -} - -// Check if the file has changed -const oldhash = crypto.createHash('sha1').setEncoding('hex') -oldhash.write(await fs.readFile(outpath)) -oldhash.end() -const newhash = crypto.createHash('sha1').setEncoding('hex') -newhash.write(indexhtml) -newhash.end() - -if (oldhash.read() !== newhash.read()) { - // Write it out - console.log('Creating dist/single-file/parchment.html') - await fs.writeFile(outpath, indexhtml) +// Write it out +console.log('Creating dist/single-file/parchment.html') +await fs.writeFile(outpath, indexhtml) - // Zip it - let zipname = `parchment-single-file.zip` - if (datemode) { - const today = new Date() - zipname = `parchment-single-file-${today.getFullYear()}-${(today.getMonth() + 1).toString().padStart(2, '0')}-${today.getDate().toString().padStart(2, '0')}.zip` - } - console.log(`Zipping dist/single-file/${zipname}`) - const result = await execFile('zip', ['-j', '-r', path.join(outdir, zipname), outpath]) - console.log(result.stdout.trim()) +// Zip it +let zipname = `parchment-single-file.zip` +if (options.date) { + const today = new Date() + zipname = `parchment-single-file-${today.getFullYear()}-${(today.getMonth() + 1).toString().padStart(2, '0')}-${today.getDate().toString().padStart(2, '0')}.zip` } -else { - console.log('Single file parchment.html is unchanged') -} \ No newline at end of file +console.log(`Zipping dist/single-file/${zipname}`) +const result = await execFile('zip', ['-j', '-r', path.join(outdir, zipname), outpath]) +console.log(result.stdout.trim()) \ No newline at end of file