Skip to content

Commit

Permalink
Update make-single-file and stop distributing it in gh-pages
Browse files Browse the repository at this point in the history
  • Loading branch information
curiousdannii committed Apr 15, 2023
1 parent 8fbf562 commit 5c384f2
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 114 deletions.
1 change: 0 additions & 1 deletion .github/workflows/test-and-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------------
Expand Down
99 changes: 99 additions & 0 deletions src/tools/single-file.js
Original file line number Diff line number Diff line change
@@ -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('<script>parchment_options = {single_file: 1}</script>')
}

// 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(`<script nomodule>${data}</script>`)
}
else if (filename === 'jquery.min.js') {
inclusions.push(`<script>${data}</script>`)
}
else if (filename === 'main.js') {
inclusions.push(`<script type="module">${data}</script>`)
}
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(`<style>${data}</style>`)
}
else if (filename.endsWith('.js')) {
inclusions.push(`<script type="text/plain" id="./${filename}">${data}</script>`)
}
else if (filename.endsWith('.wasm')) {
inclusions.push(`<script type="text/plain" id="./${filename}">${data.toString('base64')}</script>`)
}
}

// Inject into index.html
let indexhtml = files['index.html']
const gif = await Uint8Array_to_base64(files['waiting.gif'])
indexhtml = indexhtml
.replace(/<script.+?\/script>/g, '')
.replace(/<link rel="stylesheet".+?>/g, '')
.replace(/<img src="dist\/web\/waiting.gif"/, `<img src="data:image/gif;base64,${gif}"`)
.replace(/^\s+$/gm, '')

// Add a date if requested
if (options.date) {
const today = new Date()
const title = `Parchment ${today.getFullYear()}.${today.getMonth() + 1}.${today.getDate()}`
indexhtml = indexhtml
.replace(/<title.+?\/title>/, `<title>${title}</title>`)
.replace(/<\/noscript>/, `</noscript>\n<p id="footer-date">${title}</p>`)
}

// Add the inclusions
const parts = indexhtml.split(/\s*<\/head>/)
indexhtml = `${parts[0]}
${inclusions.join('\n')}
</head>${parts[1]}`

return indexhtml
}
175 changes: 63 additions & 112 deletions tools/make-single-file.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<script nomodule>${data}</script>`
}
if (filename === 'jquery.min.js') {
return `<script>${data}</script>`
}
if (filename === 'main.js') {
return `<script type="module">${data}</script>`
}
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 `<style>${data}</style>`
}
if (filename.endsWith('.js')) {
return `<script type="text/plain" id="./${filename}">${data}</script>`
}
if (filename.endsWith('.wasm')) {
return `<script type="text/plain" id="./${filename}">${data.toString('base64')}</script>`
}
return
}))

// Inject into index.html
let indexhtml = await fs.readFile(path.join(rootpath, 'index.html'), {encoding: 'utf8'})
indexhtml = indexhtml
.replace(/<script.+?\/script>/g, '')
.replace(/<link rel="stylesheet".+?>/g, '')
.replace(/<img src="dist\/web\/waiting.gif"/, `<img src="data:image/gif;base64,${await file_to_base64(path.join(webpath, 'waiting.gif'))}"`)
.replace(/^\s+$/gm, '')
const preset = process.argv[2] || 'dist'
if (!presets[preset]) {
throw new Error(`Unknown preset: ${process.argv[2]}`)
}
const options = Object.assign({}, base_options, presets[preset])

// Load files
const common = [
'ie.js',
'jquery.min.js',
'main.js',
'waiting.gif',
'web.css',
'../fonts/iosevka/iosevka-extended.woff2',
'../../index.html',
]
const terps = {
bocfel: ['bocfel-core.wasm', 'boxfel.js'],
git: ['git-core.wasm', 'git.js'],
glulxe: ['glulxe-core.wasm', 'glulxe.js'],
hugo: ['hugo-core.wasm', 'hugo.js'],
quixe: ['quixe.js'],
scare: ['scare-core.wasm', 'scare.js'],
tads: ['tads-core.wasm', 'tads.js'],
zvm: ['zvm.js'],
}

// Add a date if requested
if (datemode) {
const today = new Date()
const title = `Parchment ${today.getFullYear()}.${today.getMonth() + 1}.${today.getDate()}`
indexhtml = indexhtml
.replace(/<title.+?\/title>/, `<title>${title}</title>`)
.replace(/<\/noscript>/, `</noscript>\n<p id="footer-date">${title}</p>`)
// 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]}
<script>parchment_options = {single_file: 1}</script>
${files.filter(file => file).join('\n')}
</head>${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')
}
console.log(`Zipping dist/single-file/${zipname}`)
const result = await execFile('zip', ['-j', '-r', path.join(outdir, zipname), outpath])
console.log(result.stdout.trim())

0 comments on commit 5c384f2

Please sign in to comment.