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