Skip to content

Commit 20f694b

Browse files
committed
1 parent 5c97b69 commit 20f694b

10 files changed

Lines changed: 203 additions & 24 deletions

File tree

README.md

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,32 @@
11
# htmlifier
22

3-
The HTMLifier "converts" Scratch 3.0 projects to an HTML file by putting all the project data and the entire Scratch engine into one enormous file.
4-
It does this by making Scratch VM fetch a project, and in doing so, it tracks the assets it fetches from the project.json. It converts the fetched assets and project.json into a base64 data URI. It also fetches the code for the Scratch VM. It inserts all this in a template HTML file, which has been set up to load the project from the base64 data URIs and do other things that the Scratch VM doesn't take of, which is normally handled by [scratch-gui](https://github.com/LLK/scratch-gui/), such as variable/list monitors and ask and wait prompts.
3+
The HTMLifier "converts" Scratch 3.0 projects to an HTML file by putting all the
4+
project data and the entire Scratch engine into one enormous file.
55

6-
[hacky-file-getter.js](./hacky-file-getter.js) is based on [Scratch VM's benchmark page](https://github.com/LLK/scratch-vm/blob/develop/src/playground/benchmark.js).
6+
It does this by making Scratch VM fetch a project, and in doing so, it tracks
7+
the assets it fetches from the project.json. It converts the fetched assets and
8+
project.json into a base64 data URI. It also fetches the code for the Scratch
9+
VM. It inserts all this in a template HTML file, which has been set up to load
10+
the project from the base64 data URIs and do other things that the Scratch VM
11+
doesn't take of, which is normally handled by
12+
[scratch-gui](https://github.com/LLK/scratch-gui/), such as variable/list
13+
monitors and ask and wait prompts.
14+
15+
The HTMLifier is also available as an [npm
16+
package](https://www.npmjs.com/package/@sheeptester/htmlifier).
717

818
## Credits
919

1020
CSS by [Mr. Cringe Kid](https://scratch.mit.edu/users/mrcringekidyt/).
1121

1222
The Scratch engine is from [scratch-vm](https://github.com/LLK/scratch-vm/).
1323

14-
[download.js](http://danml.com/download.html) is used to download the HTML file.
15-
1624
[JSZip](https://stuk.github.io/jszip/) for downloading as a zip.
1725

1826
## See also
1927

20-
The Scratch mod used by the HTMLifier: [E羊icques](https://sheeptester.github.io/scratch-gui/) ([Github](https://github.com/SheepTester/scratch-gui))
28+
The Scratch mod used by the HTMLifier: [E羊icques](https://sheeptester.github.io/scratch-gui/)
29+
([Github](https://github.com/SheepTester/scratch-gui)).
2130

2231
Depending on your use case, the HTMLifier may not be the best option for you.
2332
The HTMLifier prioritizes accuracy by using almost the same engine that vanilla
@@ -43,4 +52,8 @@ deno run --allow-run --allow-read=src --allow-write=index.bundle.min.js \
4352
# Automatically build when the files change (for development)
4453
nodemon --exec "deno run --allow-all" --watch src --watch client \
4554
--ext ts,css,html,js bin/build.ts -- dev
55+
56+
# Build for Node
57+
deno run --allow-run --allow-read=src --allow-write=node/index.min.js \
58+
--allow-net=sheeptester.github.io bin/build.ts node
4659
```

bin/build.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import { vm, extensionWorker, template } from '../src/dependencies.ts'
44
import { writeAll } from 'https://deno.land/std@0.101.0/io/util.ts'
55

6-
const minify = Deno.args[0] !== 'dev'
6+
const minify = !Deno.args.includes('dev')
7+
const isNode = Deno.args.includes('node')
78

89
const decoder = new TextDecoder()
910
const encoder = new TextEncoder()
@@ -15,7 +16,10 @@ const bundleProcess = Deno.run({
1516
'--no-check',
1617
'--import-map',
1718
new URL('../import-map.json', import.meta.url).toString(),
18-
new URL('../client/index.ts', import.meta.url).toString()
19+
new URL(
20+
isNode ? '../src/htmlifier.ts' : '../client/index.ts',
21+
import.meta.url
22+
).toString()
1923
],
2024
stdout: 'piped'
2125
})
@@ -27,7 +31,32 @@ if (!status.success) {
2731
)
2832
}
2933

30-
let result = `(async () => {
34+
let result = isNode
35+
? `import fetch from 'node-fetch'
36+
import Blob from 'fetch-blob'
37+
38+
// A lame "polyfill" for FileReader
39+
class FileReader {
40+
addEventListener (_, callback) {
41+
this.callback = callback
42+
}
43+
44+
async readAsDataURL (blob) {
45+
// https://stackoverflow.com/a/62684503
46+
this.result = Buffer.from(await blob.arrayBuffer()).toString('base64')
47+
this.callback()
48+
}
49+
}
50+
51+
const dependencies_vm = '__htmlifier_VM__'
52+
const dependencies_extensionWorker = '__htmlifier_EW__'
53+
const dependencies_template = '__htmlifier_TEMP__'
54+
55+
${bundle.replace(
56+
/(dependencies_\w+)\s*:[^;]+;/g,
57+
(_, match) => `${match} : undefined;`
58+
)}`
59+
: `;(async () => {
3160
const dependencies_vm = '__htmlifier_VM__'
3261
const dependencies_extensionWorker = '__htmlifier_EW__'
3362
const dependencies_template = '__htmlifier_TEMP__'
@@ -36,8 +65,12 @@ let result = `(async () => {
3665

3766
if (minify) {
3867
console.log('Minifying...')
68+
const options = ['terser', '--compress', 'unsafe']
69+
if (isNode) {
70+
options.push('--module')
71+
}
3972
const minifyProcess = Deno.run({
40-
cmd: ['terser', '--compress', 'unsafe'],
73+
cmd: options,
4174
stdin: 'piped',
4275
stdout: 'piped'
4376
})
@@ -54,7 +87,10 @@ if (minify) {
5487

5588
console.log('Substituting dependencies...')
5689
Deno.writeTextFile(
57-
new URL('../index.bundle.min.js', import.meta.url),
90+
new URL(
91+
isNode ? '../node/index.min.js' : '../index.bundle.min.js',
92+
import.meta.url
93+
),
5894
result
5995
.replace(/['"]__htmlifier_TEMP__['"]/, () => JSON.stringify(template))
6096
.replace(/['"]__htmlifier_EW__['"]/, () => JSON.stringify(extensionWorker))

index.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,6 @@ <h2 id="credits">Credits</h2>
449449
Made by
450450
<a href="https://scratch.mit.edu/users/Sheep_maker/">Sheep_maker</a>,
451451
who used <a href="https://github.com/LLK/scratch-vm/">scratch-vm</a>,
452-
<a href="http://danml.com/download.html">download.js</a>,
453452
<a href="https://stuk.github.io/jszip/">JSZip</a>,
454453
<a href="https://deno.land/">Deno</a>,
455454
<a href="https://reactjs.org/">React</a>, and their dependencies for

node/.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules
2+
package-lock.json

node/README.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
# [HTMLifier](https://sheeptester.github.io/htmlifier/) for Node
2+
3+
Package a Scratch project inside an HTML file with no optimisations.
4+
5+
## Installation
6+
7+
```sh
8+
$ npm install @sheeptester/htmlifier
9+
```
10+
11+
## Example usage
12+
13+
The following example HTMLifies [Scratch 3.0 is
14+
here!](https://scratch.mit.edu/projects/276660763/) and writes the result to a
15+
file named `index.html`.
16+
17+
```js
18+
import fs from 'fs/promises'
19+
import Htmlifier from '@sheeptester/htmlifier'
20+
21+
async function main () {
22+
const html = await new Htmlifier()
23+
.htmlify({ type: 'id', id: '276660763' })
24+
.then(blob => blob.text())
25+
await fs.writeFile('./index.html', html)
26+
}
27+
28+
main()
29+
```
30+
31+
Because [`node-fetch`](https://www.npmjs.com/package/node-fetch) is strictly an
32+
ES module, this package is also an ES module. If you're using a CommonJS module,
33+
you can use [`esm`](https://www.npmjs.com/package/esm) to import this package.
34+
35+
```js
36+
require = require('esm')(module)
37+
const Htmlifier = require('@sheeptester/htmlifier')
38+
```
39+
40+
The HTMLifier was primarily written for Deno and the web, so it uses
41+
[`Blob`s](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and
42+
[`File`s](https://developer.mozilla.org/en-US/docs/Web/API/File) to pass binary
43+
data around. You can use
44+
[`fetch-blob`](https://www.npmjs.com/package/fetch-blob) to create `Blob`s and
45+
`File`s for the HTMLifier.
46+
47+
```js
48+
import Htmlifier from '@sheeptester/htmlifier'
49+
import { fileFrom } from 'fetch-blob/from.js'
50+
51+
async function main () {
52+
const html = await new Htmlifier()
53+
.htmlify({ type: 'file', file: await fileFrom('./project.sb3') })
54+
.then(blob => blob.text())
55+
// ...
56+
}
57+
58+
main()
59+
```
60+
61+
## Documentation
62+
63+
[Auto-generated
64+
documentation](https://doc.deno.land/https/github.com/SheepTester/htmlifier/raw/v1.0.0/src/htmlifier.ts)
65+
is available courtesy of deno doc.

node/index.min.js

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "@sheeptester/htmlifier",
3+
"version": "1.0.0",
4+
"description": "Packages a Scratch project in an HTML file with no optimisations.",
5+
"type": "module",
6+
"main": "index.min.js",
7+
"scripts": {
8+
"test": "node test.js"
9+
},
10+
"keywords": [
11+
"htmlifier",
12+
"scratch",
13+
"sb3"
14+
],
15+
"author": "SheepTester",
16+
"license": "MIT",
17+
"dependencies": {
18+
"fetch-blob": "^3.1.2",
19+
"node-fetch": "^2.6.1"
20+
}
21+
}

node/test.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// This should not throw an error
2+
3+
import Htmlifier from './index.min.js'
4+
import fetch from 'node-fetch'
5+
6+
function checkForReasonableHtml (html) {
7+
if (html.length < 1_000_000) {
8+
throw new RangeError(
9+
`Shouldn't the result be at least a megabyte instead of ${html.length} characters?`
10+
)
11+
}
12+
if (!html.startsWith('<!DOCTYPE html>')) {
13+
throw new SyntaxError(
14+
`HTML does not start with DOCTYPE. Instead, it starts with ${html.slice(
15+
0,
16+
20
17+
)}...`
18+
)
19+
}
20+
}
21+
22+
new Htmlifier()
23+
.htmlify({ type: 'id', id: '276660763' })
24+
.then(blob => blob.text())
25+
.then(checkForReasonableHtml)
26+
27+
fetch('https://sheeptester.gitlab.io/test/special_cloud_behaviours_example.sb3')
28+
.then(r => r.blob())
29+
.then(blob => new Htmlifier().htmlify({ type: 'file', file: blob }))
30+
.then(blob => blob.text())
31+
.then(checkForReasonableHtml)
32+
33+
new Htmlifier()
34+
.htmlify({
35+
type: 'url',
36+
url:
37+
'https://sheeptester.gitlab.io/test/special_cloud_behaviours_example.sb3'
38+
})
39+
.then(blob => blob.text())
40+
.then(checkForReasonableHtml)

src/get-file-extension.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
* - 'init' -> ''
77
*/
88
export default function getFileExtension (nameOrFile: string | File): string {
9-
const fileName = nameOrFile instanceof File ? nameOrFile.name : nameOrFile
9+
const fileName = typeof nameOrFile === 'string' ? nameOrFile : nameOrFile.name
1010
return fileName.includes('.') ? fileName.slice(fileName.lastIndexOf('.')) : ''
1111
}

src/htmlifier.ts

Lines changed: 13 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -348,7 +348,9 @@ export default class Htmlifier {
348348
return `./${fileName}`
349349
} else {
350350
return await getDataUrl(
351-
file instanceof Blob ? file : new Blob([file], { type: 'text/plain' })
351+
typeof file === 'string'
352+
? new Blob([file], { type: 'text/plain' })
353+
: file
352354
)
353355
}
354356
}
@@ -393,7 +395,7 @@ export default class Htmlifier {
393395
)
394396
const extensionScripts: string[] = []
395397
for (const extensionSource of extensions) {
396-
if (extensionSource instanceof File) {
398+
if (typeof extensionSource !== 'string') {
397399
extensionScripts.push(
398400
outputZip
399401
? await registerFile(
@@ -446,20 +448,20 @@ export default class Htmlifier {
446448
injectedJs.push({
447449
type: 'url',
448450
url:
449-
source instanceof File
450-
? await registerFile(source.name, await source.text())
451-
: await registerFile(
451+
typeof source === 'string'
452+
? await registerFile(
452453
source + '.js',
453454
await fetch(source).then(toBlob)
454455
)
456+
: await registerFile(source.name, await source.text())
455457
})
456458
} else {
457459
injectedJs.push({
458460
type: 'inline',
459461
source:
460-
source instanceof File
461-
? await source.text()
462-
: await fetch(source).then(toText)
462+
typeof source === 'string'
463+
? await fetch(source).then(toText)
464+
: await source.text()
463465
})
464466
}
465467
}
@@ -543,12 +545,12 @@ export default class Htmlifier {
543545
}
544546
if (loadingImage) {
545547
const imageUrl =
546-
loadingImage instanceof File
547-
? await registerFile(
548+
typeof loadingImage === 'string'
549+
? loadingImage
550+
: await registerFile(
548551
'loading' + getFileExtension(loadingImage),
549552
loadingImage
550553
)
551-
: loadingImage
552554
html = html.replace(
553555
'{LOADING_IMAGE}',
554556
() => `<img src="${escapeHtml(imageUrl)}" id="loading-image">`

0 commit comments

Comments
 (0)