From f8beeac8beac55c895db1caaf66086e99e8e9521 Mon Sep 17 00:00:00 2001 From: neko <225498830+neko782@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:20:26 +0000 Subject: [PATCH 1/2] add turbowarp packager --- package.json | 1 + src/handlers/index.ts | 2 ++ src/handlers/turbowarp.ts | 61 +++++++++++++++++++++++++++++++++++++++ vite.config.js | 4 +++ 4 files changed, 68 insertions(+) create mode 100644 src/handlers/turbowarp.ts diff --git a/package.json b/package.json index f24bda1e..05924a10 100644 --- a/package.json +++ b/package.json @@ -87,6 +87,7 @@ "three": "^0.182.0", "three-bvh-csg": "^0.0.17", "three-mesh-bvh": "^0.9.8", + "turbowarp-packager-browser": "3.11.1", "ts-flp": "^1.0.3", "verovio": "^6.0.1", "vexflow": "^5.0.0", diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 38ee48e4..4a9c7d43 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -34,6 +34,7 @@ import cgbiToPngHandler from "./cgbi-to-png.ts"; import batToExeHandler from "./batToExe.ts"; import textEncodingHandler from "./textEncoding.ts"; import jsonToCHandler from "./jsonToC.ts"; +import turbowarpHandler from "./turbowarp.ts"; import sb3ToHtmlHandler from "./sb3tohtml.ts"; import libopenmptHandler from "./libopenmpt.ts"; import { midiCodecHandler, midiSynthHandler } from "./midi.ts"; @@ -101,6 +102,7 @@ try { handlers.push(new flptojsonHandler()) } catch (_) { }; try { handlers.push(new floHandler()) } catch (_) { }; try { handlers.push(new cgbiToPngHandler()) } catch (_) { }; try { handlers.push(new batToExeHandler()) } catch (_) { }; +try { handlers.push(new turbowarpHandler()) } catch (_) { }; try { handlers.push(new sb3ToHtmlHandler()) } catch (_) { }; try { handlers.push(new textEncodingHandler()) } catch (_) { }; try { handlers.push(new jsonToCHandler()) } catch (_) { }; diff --git a/src/handlers/turbowarp.ts b/src/handlers/turbowarp.ts new file mode 100644 index 00000000..f9f38ca0 --- /dev/null +++ b/src/handlers/turbowarp.ts @@ -0,0 +1,61 @@ +// file: turbowarp.ts + +import type { FileData, FileFormat, FormatHandler } from "../FormatHandler.ts"; +import CommonFormats from "src/CommonFormats.ts"; +import { Packager, largeAssets, downloadProject } from "turbowarp-packager-browser"; + +// patching some assets +largeAssets.scaffolding.src = "/convert/js/turbowarp-scaffolding/scaffolding-full.js"; +largeAssets["scaffolding-min"].src = "/convert/js/turbowarp-scaffolding/scaffolding-min.js"; +largeAssets.addons.src = "/convert/js/turbowarp-scaffolding/addons.js"; + +class turbowarpHandler implements FormatHandler { + + public name: string = "turbowarp"; + public supportedFormats: FileFormat[] = [ + { + name: "Scratch 3 Project", + format: "sb3", + extension: "sb3", + mime: "application/x.scratch.sb3", + from: true, + to: false, + internal: "sb3", + category: "archive", + lossless: true, + }, + CommonFormats.HTML.builder("html") + .allowTo() + ]; + public ready: boolean = false; + + async init () { + this.ready = true; + } + + async doConvert ( + inputFiles: FileData[], + inputFormat: FileFormat, + outputFormat: FileFormat + ): Promise { + const outputFiles: FileData[] = []; + for (const inputFile of inputFiles) { + const project = await downloadProject(inputFile.bytes); + + const packager = new Packager(); + packager.project = project; + packager.options.target = "html"; + + const bytes = (await packager.package()).data; + + outputFiles.push({ + name: inputFile.name.replace(/\.sb3$/, ".html"), + bytes + }); + } + return outputFiles; + } + +} + +export default turbowarpHandler; \ No newline at end of file diff --git a/vite.config.js b/vite.config.js index 87bab18f..e1943a47 100644 --- a/vite.config.js +++ b/vite.config.js @@ -66,6 +66,10 @@ export default defineConfig({ src: "src/handlers/tarCompressed/liblzma.wasm", dest: "wasm" }, + { + src: "node_modules/turbowarp-packager-browser/dist/scaffolding/*", + dest: "js/turbowarp-scaffolding" + }, ] }), tsconfigPaths() From 1024cf9685938a5f451e133630e4e3bedaf226c0 Mon Sep 17 00:00:00 2001 From: neko <225498830+neko782@users.noreply.github.com> Date: Tue, 17 Mar 2026 18:20:56 +0000 Subject: [PATCH 2/2] remove sb3tohtml --- src/handlers/index.ts | 2 - src/handlers/sb3tohtml.ts | 184 -------------------------------------- 2 files changed, 186 deletions(-) delete mode 100644 src/handlers/sb3tohtml.ts diff --git a/src/handlers/index.ts b/src/handlers/index.ts index 4a9c7d43..3abc0893 100644 --- a/src/handlers/index.ts +++ b/src/handlers/index.ts @@ -35,7 +35,6 @@ import batToExeHandler from "./batToExe.ts"; import textEncodingHandler from "./textEncoding.ts"; import jsonToCHandler from "./jsonToC.ts"; import turbowarpHandler from "./turbowarp.ts"; -import sb3ToHtmlHandler from "./sb3tohtml.ts"; import libopenmptHandler from "./libopenmpt.ts"; import { midiCodecHandler, midiSynthHandler } from "./midi.ts"; import lzhHandler from "./lzh.ts"; @@ -103,7 +102,6 @@ try { handlers.push(new floHandler()) } catch (_) { }; try { handlers.push(new cgbiToPngHandler()) } catch (_) { }; try { handlers.push(new batToExeHandler()) } catch (_) { }; try { handlers.push(new turbowarpHandler()) } catch (_) { }; -try { handlers.push(new sb3ToHtmlHandler()) } catch (_) { }; try { handlers.push(new textEncodingHandler()) } catch (_) { }; try { handlers.push(new jsonToCHandler()) } catch (_) { }; try { handlers.push(new libopenmptHandler()) } catch (_) { }; diff --git a/src/handlers/sb3tohtml.ts b/src/handlers/sb3tohtml.ts deleted file mode 100644 index d8c2e824..00000000 --- a/src/handlers/sb3tohtml.ts +++ /dev/null @@ -1,184 +0,0 @@ -import type { FileData, FileFormat, FormatHandler } from "../FormatHandler.ts"; -import CommonFormats from "src/CommonFormats.ts"; -import JSZip from "jszip"; -import * as mime from "mime"; -import normalizeMimeType from "../normalizeMimeType.ts"; - -function resolveMime(fmt: string): string { - const ext = String(fmt ?? "").toLowerCase(); - const anyMime = mime as unknown as Record; - - const guessed = - anyMime.getType?.(ext) ?? - anyMime.get?.(ext) ?? - anyMime.default?.getType?.(ext) ?? - anyMime.default?.get?.(ext) ?? - null; - - return normalizeMimeType(guessed ?? "application/octet-stream"); -} - -class sb3ToHtmlHandler implements FormatHandler { - public name = "sb3tohtml"; - public supportedFormats?: FileFormat[]; - public ready = false; - - async init() { - this.supportedFormats = [ - { - name: "Scratch 3 Project", - format: "sb3", - extension: "sb3", - mime: "application/x.scratch.sb3", - from: true, - to: false, - internal: "sb3", - category: "archive", - lossless: false, - }, - CommonFormats.HTML.builder("html") - .allowTo() - ]; - this.ready = true; - } - - async doConvert( - inputFiles: FileData[] - ): Promise { - const inputFile = inputFiles[0]; - const zip = await JSZip.loadAsync(inputFile.bytes); - - const projectJsonStr = await zip.file("project.json")!.async("string"); - const project = JSON.parse(projectJsonStr); - - function arrayBufferToBase64(ab: ArrayBuffer): string { - const bytes = new Uint8Array(ab); - const chunk = 0x8000; - let binary = ""; - for (let i = 0; i < bytes.length; i += chunk) { - binary += String.fromCharCode(...bytes.subarray(i, i + chunk)); - } - return btoa(binary); - } - - const parts: string[] = []; - parts.push(` - - - - -${escapeHtml(inputFile.name.replace(/\.sb3$/i, ""))} - - - -
-

${escapeHtml(inputFile.name.replace(/\.sb3$/i, ""))}

-`); - - for (const target of project.targets || []) { - parts.push(`
`); - const title = target.isStage ? "Stage" : `Sprite: ${escapeHtml(target.name || "unnamed")}`; - parts.push(`

${title}

`); - - const scratchTexts: string[] = []; - if (target.blocks) { - const blocks = target.blocks; - for (const blockId in blocks) { - const block = blocks[blockId]; - if ( - block && - block.topLevel === true && - block.parent === null && - block.shadow !== true && - typeof block.opcode === "string" - ) { - scratchTexts.push(JSON.stringify(block, null, 2)); - } - } - } - if (scratchTexts.length > 0) { - parts.push(`
${escapeHtml(scratchTexts.join("\n\n"))}
`); - } - - if (target.costumes && target.costumes.length > 0) { - parts.push(`

${target.isStage ? "Backdrops" : "Costumes"}

`); - parts.push(`
`); - for (const costume of target.costumes) { - const assetPath = `${costume.assetId}.${costume.dataFormat}`; - const file = zip.file(assetPath); - if (!file) continue; - - const ab = await file.async("arraybuffer"); - const mimeType = resolveMime(costume.dataFormat || assetPath); - const b64 = arrayBufferToBase64(ab); - const dataUrl = `data:${mimeType};base64,${b64}`; - - parts.push(`
-
${escapeHtml(costume.name || "")}
- ${escapeHtml(costume.name || -
`); - } - parts.push(`
`); - } - - if (target.sounds && target.sounds.length > 0) { - parts.push(`

Sounds

`); - parts.push(`
`); - for (const sound of target.sounds) { - const md5ext = sound.md5ext || `${sound.assetId}.${sound.format}`; - const file = zip.file(md5ext); - if (!file) { - parts.push(`
${escapeHtml(sound.name || "(missing audio)")}
`); - continue; - } - const ab = await file.async("arraybuffer"); - const mime = resolveMime(sound.format || md5ext); - const b64 = arrayBufferToBase64(ab); - const dataUrl = `data:${mime};base64,${b64}`; - - parts.push(`
-
${escapeHtml(sound.name || "")}
- -
`); - } - parts.push(`
`); - } - - parts.push(`
`); - } - - parts.push(`
- -`); - - const html = parts.join("\n"); - const encoder = new TextEncoder(); - const htmlBytes = encoder.encode(html); - - return [ - { - name: inputFile.name.replace(/\.sb3$/i, "") + ".html", - bytes: new Uint8Array(htmlBytes), - }, - ]; - } -} - -export default sb3ToHtmlHandler; - -function escapeHtml(s: string): string { - return String(s ?? "") - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -}