Files
pandoc/wasm/pandoc.js

168 lines
6.1 KiB
JavaScript

/* pandoc.js: JavaScript interface to pandoc.wasm.
Copyright (c) 2025 Tweag I/O Limited and John MacFarlane. MIT License.
Interface:
await convert(options, stdin, files)
- options is a JavaScript object representing pandoc options: this should
correspond to the format used in pandoc's default files.
- stdin is a string or nil
- files is a JavaScript object whose keys are filenames and whose values
are the data in the corresponding file, as Blobs.
The return value is a JavaScript object with 3 properties, stdout,
stderr, and warnings, all strings. warnings is a JSON-encoded
version of the warnings produced by pandoc. If the pandoc process
produces an output file, it will be added to files.
await query(options)
- options is a JavaScript object with a 'query' property and in
some cases a 'format' property. Possible queries include
'version', 'highlight-styles', 'highlight-languages', 'input-formats',
'output-formats', 'default-template' (requires 'format'),
and 'extensions-for-format' (requires 'format').
The return value is a JavaScript string or in some cases a list
of strings.
*/
import {
WASI,
OpenFile,
File,
ConsoleStdout,
PreopenDirectory,
} from "https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.3.0/dist/index.js";
const args = ["pandoc.wasm", "+RTS", "-H64m", "-RTS"];
const env = [];
const in_file = new File(new Uint8Array(), { readonly: true });
const out_file = new File(new Uint8Array(), { readonly: false });
const err_file = new File(new Uint8Array(), { readonly: false });
const warnings_file = new File(new Uint8Array(), { readonly: false });
const fileSystem = new Map();
const fds = [
new OpenFile(new File(new Uint8Array(), { readonly: true })),
ConsoleStdout.lineBuffered((msg) => console.log(`[WASI stdout] ${msg}`)),
ConsoleStdout.lineBuffered((msg) => console.warn(`[WASI stderr] ${msg}`)),
new PreopenDirectory("/", fileSystem),
];
const options = { debug: false };
const wasi = new WASI(args, env, fds, options);
const { instance } = await WebAssembly.instantiateStreaming(
fetch("./pandoc.wasm?sha1=SHA1_PANDOC_WASM"),
{
wasi_snapshot_preview1: wasi.wasiImport,
}
);
wasi.initialize(instance);
instance.exports.__wasm_call_ctors();
function memory_data_view() {
return new DataView(instance.exports.memory.buffer);
}
const argc_ptr = instance.exports.malloc(4);
memory_data_view().setUint32(argc_ptr, args.length, true);
const argv = instance.exports.malloc(4 * (args.length + 1));
for (let i = 0; i < args.length; ++i) {
const arg = instance.exports.malloc(args[i].length + 1);
new TextEncoder().encodeInto(
args[i],
new Uint8Array(instance.exports.memory.buffer, arg, args[i].length)
);
memory_data_view().setUint8(arg + args[i].length, 0);
memory_data_view().setUint32(argv + 4 * i, arg, true);
}
memory_data_view().setUint32(argv + 4 * args.length, 0, true);
const argv_ptr = instance.exports.malloc(4);
memory_data_view().setUint32(argv_ptr, argv, true);
instance.exports.hs_init_with_rtsopts(argc_ptr, argv_ptr);
export async function query(options) {
const opts_str = JSON.stringify(options);
const opts_bytes = new TextEncoder().encode(opts_str);
const opts_ptr = instance.exports.malloc(opts_bytes.length);
new Uint8Array(instance.exports.memory.buffer, opts_ptr, opts_bytes.length)
.set(opts_bytes);
// add input files to fileSystem
fileSystem.clear()
const out_file = new File(new Uint8Array(), { readonly: false });
const err_file = new File(new Uint8Array(), { readonly: false });
fileSystem.set("stdout", out_file);
fileSystem.set("stderr", err_file);
instance.exports.query(opts_ptr, opts_bytes.length);
const err_text = new TextDecoder("utf-8", { fatal: true }).decode(err_file.data);
if (err_text) console.log(err_text);
const out_text = new TextDecoder("utf-8", { fatal: true }).decode(out_file.data);
return JSON.parse(out_text);
}
export async function convert(options, stdin, files) {
const opts_str = JSON.stringify(options);
const opts_bytes = new TextEncoder().encode(opts_str);
const opts_ptr = instance.exports.malloc(opts_bytes.length);
new Uint8Array(instance.exports.memory.buffer, opts_ptr, opts_bytes.length)
.set(opts_bytes);
// add input files to fileSystem
fileSystem.clear()
const in_file = new File(new Uint8Array(), { readonly: true });
const out_file = new File(new Uint8Array(), { readonly: false });
const err_file = new File(new Uint8Array(), { readonly: false });
const warnings_file = new File(new Uint8Array(), { readonly: false });
fileSystem.set("stdin", in_file);
fileSystem.set("stdout", out_file);
fileSystem.set("stderr", err_file);
fileSystem.set("warnings", warnings_file);
for (const file in files) {
await addFile(file, files[file], true);
}
// add output file if any
if (options["output-file"]) {
await addFile(options["output-file"], new Blob(), false);
}
// add media file for extracted media
if (options["extract-media"]) {
await addFile(options["extract-media"], new Blob(), false);
}
if (stdin) {
in_file.data = new TextEncoder().encode(stdin);
}
instance.exports.convert(opts_ptr, opts_bytes.length);
if (options["output-file"]) {
files[options["output-file"]] =
new Blob([fileSystem.get(options["output-file"]).data]);
}
if (options["extract-media"]) {
const mediaFile = fileSystem.get(options["extract-media"]);
if (mediaFile && mediaFile.data && mediaFile.data.length > 0) {
files[options["extract-media"]] =
new Blob([mediaFile.data], { type: 'application/zip' });
}
}
const rawWarnings = new TextDecoder("utf-8", { fatal: true })
.decode(warnings_file.data);
let warnings = [];
if (rawWarnings) {
warnings = JSON.parse(rawWarnings);
}
return {
stdout: new TextDecoder("utf-8", { fatal: true }).decode(out_file.data),
stderr: new TextDecoder("utf-8", { fatal: true }).decode(err_file.data),
warnings: warnings
};
}
async function addFile(filename, blob, readonly) {
const buffer = await blob.arrayBuffer();
const file = new File(new Uint8Array(buffer), { readonly: readonly });
fileSystem.set(filename, file);
}