├── .github └── workflows │ └── build.yml ├── LICENSE ├── README.md └── frontend ├── index.html └── index.js /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | 3 | on: 4 | merge_group: 5 | pull_request: 6 | push: 7 | branches: 8 | - master 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build: 13 | name: build 14 | runs-on: ubuntu-latest 15 | permissions: 16 | pages: write 17 | id-token: write 18 | steps: 19 | 20 | - name: setup-alex-happy 21 | run: | 22 | pushd "$(mktemp -d)" 23 | cabal path --installdir >> "$GITHUB_PATH" 24 | cabal update 25 | cabal install \ 26 | alex \ 27 | happy 28 | popd 29 | 30 | - name: setup-ghc-wasm 31 | run: | 32 | pushd "$(mktemp -d)" 33 | curl -f -L --retry 5 https://gitlab.haskell.org/haskell-wasm/ghc-wasm-meta/-/archive/master/ghc-wasm-meta-master.tar.gz | tar xz --strip-components=1 34 | FLAVOUR=9.12 ./setup.sh 35 | ~/.ghc-wasm/add_to_github_path.sh 36 | popd 37 | 38 | - name: checkout 39 | uses: actions/checkout@v4 40 | 41 | - name: checkout 42 | uses: actions/checkout@v4 43 | with: 44 | repository: haskell-wasm/pandoc 45 | ref: wasm 46 | path: pandoc 47 | 48 | - name: gen-plan-json 49 | run: | 50 | pushd pandoc 51 | wasm32-wasi-cabal build pandoc-cli --dry-run 52 | popd 53 | 54 | - name: wasm-cabal-cache 55 | uses: actions/cache@v4 56 | with: 57 | key: wasm-cabal-cache-${{ hashFiles('pandoc/dist-newstyle/cache/plan.json') }} 58 | restore-keys: wasm-cabal-cache- 59 | path: | 60 | ~/.ghc-wasm/.cabal/store 61 | pandoc/dist-newstyle 62 | 63 | - name: build 64 | run: | 65 | pushd pandoc 66 | wasm32-wasi-cabal build pandoc-cli 67 | popd 68 | 69 | - name: dist 70 | run: | 71 | mkdir dist 72 | wasm-opt --low-memory-unused --converge --gufa --flatten --rereloop -Oz $(find pandoc -type f -name pandoc.wasm) -o dist/pandoc.wasm 73 | cp frontend/*.html frontend/*.js dist 74 | 75 | - name: test 76 | run: | 77 | wasmtime run --dir $PWD::/ -- dist/pandoc.wasm pandoc/README.md -o pandoc/README.rst 78 | head --lines=20 pandoc/README.rst 79 | 80 | - name: upload-pages-artifact 81 | uses: actions/upload-pages-artifact@v3 82 | with: 83 | path: dist 84 | retention-days: 90 85 | 86 | - name: deploy-pages 87 | uses: actions/deploy-pages@v4 88 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) Tweag I/O Limited. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `pandoc-wasm` 2 | 3 | [![Chat on Matrix](https://matrix.to/img/matrix-badge.svg)](https://matrix.to/#/#haskell-wasm:matrix.terrorjack.com) 4 | 5 | The latest version of `pandoc` CLI compiled as a standalone 6 | `wasm32-wasi` module that can be run by engines like `wasmtime` as 7 | well as browsers. 8 | 9 | ## [Live demo](https://tweag.github.io/pandoc-wasm) 10 | 11 | Stdin on the left, stdout on the right, command line arguments at the 12 | bottom. No convert button, output is produced dynamically as input 13 | changes. 14 | 15 | You're also more than welcome to fetch the 16 | [`pandoc.wasm`](https://tweag.github.io/pandoc-wasm/pandoc.wasm) 17 | module and make your own customized app. `pandoc.wasm` is fully 18 | `wasm32-wasi` compliant and doesn't make use of any JSFFI feature in 19 | the ghc wasm backend. 20 | 21 | ## Building 22 | 23 | `pandoc.wasm` is built with 9.12 flavour of ghc wasm backend in CI, 24 | which can be installed via 25 | [`ghc-wasm-meta`](https://gitlab.haskell.org/haskell-wasm/ghc-wasm-meta). You 26 | need at least 9.10 since it's the earliest major version with (my 27 | non-official) backports for ghc wasm backend's Template Haskell & ghci 28 | support. 29 | 30 | It's built using my 31 | [fork](https://github.com/haskell-wasm/pandoc/tree/wasm) which is 32 | based on latest `pandoc` release and patches dependencies, cabal 33 | config as well as some module code to make things compilable to wasm: 34 | 35 | - No http client/server functionality. `wasip1` doesn't have proper 36 | sockets support anyway, and support for future versions of wasi is 37 | not on my radar for now. 38 | - No lua support. lua requires `setjmp`/`longjmp` which already work 39 | in `wasi-libc` to some extent, but that requires wasm exception 40 | handling feature which is not supported by `wasmtime` yet. 41 | 42 | Other functionalities should just work, if not feel free to file a bug 43 | report :) 44 | 45 | ## Acknowledgements 46 | 47 | Thanks to John MacFarlane and all the contributors who made `pandoc` 48 | possible: a fantastic tool that has benefited many developers and is a 49 | source of pride for the Haskell community! 50 | 51 | Thanks to all past efforts of using `asterius` to compile `pandoc` to 52 | wasm, including but not limited to: 53 | 54 | - George Stagg's [`pandoc-wasm`](https://github.com/georgestagg/pandoc-wasm) 55 | - Yuto Takahashi's [`wasm-pandoc`](https://github.com/y-taka-23/wasm-pandoc) 56 | - My legacy asterius pandoc [demo](https://asterius.netlify.app/demo/pandoc/pandoc.html) 57 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | pandoc-wasm playground 7 | 56 | 57 | 58 |
59 | 64 | 69 |
70 |
71 | 72 |
73 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /frontend/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | WASI, 3 | OpenFile, 4 | File, 5 | ConsoleStdout, 6 | PreopenDirectory, 7 | } from "https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.3.0/dist/index.js"; 8 | 9 | const args = ["pandoc.wasm", "+RTS", "-H64m", "-RTS"]; 10 | const env = []; 11 | const in_file = new File(new Uint8Array(), { readonly: true }); 12 | const out_file = new File(new Uint8Array(), { readonly: false }); 13 | const fds = [ 14 | new OpenFile(new File(new Uint8Array(), { readonly: true })), 15 | ConsoleStdout.lineBuffered((msg) => console.log(`[WASI stdout] ${msg}`)), 16 | ConsoleStdout.lineBuffered((msg) => console.warn(`[WASI stderr] ${msg}`)), 17 | new PreopenDirectory("/", [ 18 | ["in", in_file], 19 | ["out", out_file], 20 | ]), 21 | ]; 22 | const options = { debug: false }; 23 | const wasi = new WASI(args, env, fds, options); 24 | const { instance } = await WebAssembly.instantiateStreaming( 25 | fetch("./pandoc.wasm"), 26 | { 27 | wasi_snapshot_preview1: wasi.wasiImport, 28 | } 29 | ); 30 | 31 | wasi.initialize(instance); 32 | instance.exports.__wasm_call_ctors(); 33 | 34 | function memory_data_view() { 35 | return new DataView(instance.exports.memory.buffer); 36 | } 37 | 38 | const argc_ptr = instance.exports.malloc(4); 39 | memory_data_view().setUint32(argc_ptr, args.length, true); 40 | const argv = instance.exports.malloc(4 * (args.length + 1)); 41 | for (let i = 0; i < args.length; ++i) { 42 | const arg = instance.exports.malloc(args[i].length + 1); 43 | new TextEncoder().encodeInto( 44 | args[i], 45 | new Uint8Array(instance.exports.memory.buffer, arg, args[i].length) 46 | ); 47 | memory_data_view().setUint8(arg + args[i].length, 0); 48 | memory_data_view().setUint32(argv + 4 * i, arg, true); 49 | } 50 | memory_data_view().setUint32(argv + 4 * args.length, 0, true); 51 | const argv_ptr = instance.exports.malloc(4); 52 | memory_data_view().setUint32(argv_ptr, argv, true); 53 | 54 | instance.exports.hs_init_with_rtsopts(argc_ptr, argv_ptr); 55 | 56 | export function pandoc(args_str, in_str) { 57 | const args_ptr = instance.exports.malloc(args_str.length); 58 | new TextEncoder().encodeInto( 59 | args_str, 60 | new Uint8Array(instance.exports.memory.buffer, args_ptr, args_str.length) 61 | ); 62 | in_file.data = new TextEncoder().encode(in_str); 63 | instance.exports.wasm_main(args_ptr, args_str.length); 64 | return new TextDecoder("utf-8", { fatal: true }).decode(out_file.data); 65 | } 66 | --------------------------------------------------------------------------------