├── .gitignore ├── pandoc-version.txt ├── package.json ├── LICENSE ├── .pre-commit-config.yaml ├── demo ├── index.js └── index.html ├── README.md ├── src └── index.js ├── biome.json ├── patch └── pandoc.patch └── .github └── workflows └── build.yml /.gitignore: -------------------------------------------------------------------------------- 1 | /pandoc 2 | -------------------------------------------------------------------------------- /pandoc-version.txt: -------------------------------------------------------------------------------- 1 | 3.7.0.1 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wasm-pandoc", 3 | "version": "0.8.0", 4 | "description": "Pandoc transpiled as WASM to be used in browsers.", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/fiduswriter/wasm-pandoc.git" 12 | }, 13 | "homepage": "https://github.com/fiduswriter/wasm-pandoc", 14 | "bugs": { 15 | "url": "https://github.com/fiduswriter/wasm-pandoc/issues" 16 | }, 17 | "keywords": [ 18 | "pandoc", 19 | "wasm" 20 | ], 21 | "author": "Johannes Wilm", 22 | "license": "MIT", 23 | "dependencies": { 24 | "@bjorn3/browser_wasi_shim": "^0.4.1" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v5.0.0 4 | hooks: 5 | - id: check-yaml 6 | # Check YAML files for syntax errors, particularly useful for GitHub Actions workflows 7 | files: \.ya?ml$ 8 | - id: trailing-whitespace 9 | # Ensure no trailing whitespace 10 | - id: end-of-file-fixer 11 | # Ensure files end with a newline 12 | - id: check-added-large-files 13 | # Prevent large files from being committed 14 | args: ['--maxkb=500'] 15 | - id: check-merge-conflict 16 | # Check for merge conflict strings 17 | - id: check-json 18 | # Validate JSON files 19 | - id: detect-private-key 20 | # Prevent accidental commit of private keys 21 | 22 | - repo: https://github.com/rhysd/actionlint 23 | rev: v1.7.7 24 | hooks: 25 | - id: actionlint 26 | # Lint GitHub Actions workflows 27 | files: ^\.github/workflows/ 28 | 29 | - repo: https://github.com/biomejs/pre-commit 30 | rev: v1.9.4 31 | hooks: 32 | - id: biome-check 33 | # #entry: biome check --files-ignore-unknown=true --no-errors-on-unmatched --fix --unsafe 34 | # additional_dependencies: ["@biomejs/biome@1.9.2"] 35 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | ConsoleStdout, 3 | File, 4 | OpenFile, 5 | PreopenDirectory, 6 | WASI 7 | } from "https://cdn.jsdelivr.net/npm/@bjorn3/browser_wasi_shim@0.4.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( 61 | instance.exports.memory.buffer, 62 | args_ptr, 63 | args_str.length 64 | ) 65 | ) 66 | in_file.data = new TextEncoder().encode(in_str) 67 | instance.exports.wasm_main(args_ptr, args_str.length) 68 | return new TextDecoder("utf-8", {fatal: true}).decode(out_file.data) 69 | } 70 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | wasm-pandoc playground 7 | 56 | 57 | 58 |
59 | 64 | 69 |
70 |
71 | 72 |
73 | 95 | 96 | 97 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `wasm-pandoc` 2 | 3 | **Looking for maintainer:** Johannes Wilm has temporarily taken over maintainership of this package due to there being no package on NPM. 4 | However, he knows very little about wasm and haskell and would like for someone else to take this package again. 5 | 6 | 7 | The latest version of `pandoc` CLI compiled as a standalone `wasm32-wasi` module that can be run by browsers. 8 | 9 | ## [Live demo](https://fiduswriter.github.io/wasm-pandoc) 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 | 16 | ## To use 17 | 18 | 1. Make `wasm-pandoc` a dependency in your project.json. 19 | 20 | 2. In your bundler mark "wasm" as an asset/resource. For example in rspack, in your config file: 21 | 22 | ```js 23 | module.exports = { 24 | ... 25 | module: { 26 | ... 27 | rules: [ 28 | ... 29 | { 30 | test: /\.(wasm)$/, 31 | type: "asset/resource" 32 | } 33 | ... 34 | ] 35 | ... 36 | } 37 | ... 38 | } 39 | ``` 40 | 41 | 3. Import `pandoc` from `wasm-pandoc` like this: 42 | 43 | ```js 44 | import { pandoc } from "wasm-pandoc" 45 | ``` 46 | 47 | 4. Execute it like this (it's async): 48 | 49 | ```js 50 | const output = await pandoc( 51 | '-s -f json -t markdown', // command line switches 52 | inputFileContents, // string for text formats or blob for binary formats 53 | [ // Additional files - for example bibliography or images 54 | { 55 | filename: 'image13.png', 56 | contents: ..., // string for text formats or blob for binary formats 57 | }, 58 | ... 59 | ] 60 | ) 61 | 62 | console.log(output) 63 | 64 | { 65 | out: '...', 66 | mediaFiles: Map {'media': Map {'image1.jpg' => Blob, 'image2.png' => Blob, ...}} 67 | } 68 | 69 | ``` 70 | 71 | `out` will either be a string (for text formats) or a Blob for binary formats of the main output. `mediaFiles` will be a map of all additional dirs/files that pandoc has created during the process. 72 | 73 | 74 | 75 | ## Acknowledgements 76 | 77 | Thanks to John MacFarlane and all the contributors who made `pandoc` 78 | possible: a fantastic tool that has benefited many developers and is a 79 | source of pride for the Haskell community! 80 | 81 | Thanks to all efforts to make `pandoc` run with wasm, including but not limited to: 82 | 83 | - amesgen [`Don't patch out network`](https://github.com/haskell-wasm/pandoc/pull/1) 84 | - Cheng Shao [`pandoc-wasm`](https://github.com/tweag/pandoc-wasm) 85 | - George Stagg's [`pandoc-wasm`](https://github.com/georgestagg/pandoc-wasm) 86 | - Yuto Takahashi's [`wasm-pandoc`](https://github.com/y-taka-23/wasm-pandoc) 87 | - TerrorJack's asterius pandoc [demo](https://asterius.netlify.app/demo/pandoc/pandoc.html) 88 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import { 2 | ConsoleStdout, 3 | File, 4 | OpenFile, 5 | PreopenDirectory, 6 | WASI 7 | } from "@bjorn3/browser_wasi_shim" 8 | import pandocWasmLocation from "./pandoc.wasm" 9 | 10 | const pandocWasmFetch = await fetch(pandocWasmLocation) 11 | const pandocWasm = await pandocWasmFetch.arrayBuffer() 12 | const args = ["pandoc.wasm", "+RTS", "-H64m", "-RTS"] 13 | const env = [] 14 | const inFile = new File(new Uint8Array(), { 15 | readonly: true 16 | }) 17 | const outFile = new File(new Uint8Array(), { 18 | readonly: false 19 | }) 20 | 21 | async function toUint8Array(inData) { 22 | let uint8Array 23 | 24 | if (typeof inData === "string") { 25 | // If inData is a text string, convert it to a Uint8Array 26 | const encoder = new TextEncoder() 27 | uint8Array = encoder.encode(inData) 28 | } else if (inData instanceof Blob) { 29 | // If inData is a Blob, read it as an ArrayBuffer and then convert to Uint8Array 30 | const arrayBuffer = await inData.arrayBuffer() 31 | uint8Array = new Uint8Array(arrayBuffer) 32 | } else { 33 | throw new Error("Unsupported type: inData must be a string or a Blob") 34 | } 35 | 36 | return uint8Array 37 | } 38 | 39 | const textDecoder = new TextDecoder("utf-8", { 40 | fatal: true 41 | }) 42 | 43 | function convertData(data) { 44 | let outData 45 | try { 46 | // Attempt to decode the data as UTF-8 text 47 | // Return as string if successful 48 | outData = textDecoder.decode(data) 49 | } catch (_e) { 50 | // If decoding fails, assume it's binary data and return as Blob 51 | outData = new Blob([data]) 52 | } 53 | return outData 54 | } 55 | 56 | function convertItem(name, value) { 57 | if (value.contents) { 58 | // directory 59 | return [ 60 | name, 61 | new Map( 62 | [...value.contents].map(([name, value]) => 63 | convertItem(name, value) 64 | ) 65 | ) 66 | ] 67 | } else if (value.data) { 68 | // file 69 | return [name, convertData(value.data)] 70 | } 71 | } 72 | 73 | export async function pandoc(args_str, inData, resources = []) { 74 | const files = [ 75 | ["in", inFile], 76 | ["out", outFile] 77 | ] 78 | 79 | for await (const resource of resources) { 80 | const contents = await toUint8Array(resource.contents) 81 | files.push([ 82 | resource.filename, 83 | new File(contents, { 84 | readonly: true 85 | }) 86 | ]) 87 | } 88 | 89 | const rootDir = new PreopenDirectory("/", files) 90 | 91 | const fds = [ 92 | new OpenFile( 93 | new File(new Uint8Array(), { 94 | readonly: true 95 | }) 96 | ), 97 | ConsoleStdout.lineBuffered(msg => console.log(`[WASI stdout] ${msg}`)), 98 | ConsoleStdout.lineBuffered(msg => console.warn(`[WASI stderr] ${msg}`)), 99 | rootDir 100 | ] 101 | const options = { 102 | debug: false 103 | } 104 | const wasi = new WASI(args, env, fds, options) 105 | 106 | const {instance} = await WebAssembly.instantiate(pandocWasm, { 107 | wasi_snapshot_preview1: wasi.wasiImport 108 | }) 109 | 110 | wasi.initialize(instance) 111 | instance.exports.__wasm_call_ctors() 112 | 113 | function memory_data_view() { 114 | return new DataView(instance.exports.memory.buffer) 115 | } 116 | 117 | const argc_ptr = instance.exports.malloc(4) 118 | memory_data_view().setUint32(argc_ptr, args.length, true) 119 | const argv = instance.exports.malloc(4 * (args.length + 1)) 120 | for (let i = 0; i < args.length; ++i) { 121 | const arg = instance.exports.malloc(args[i].length + 1) 122 | new TextEncoder().encodeInto( 123 | args[i], 124 | new Uint8Array(instance.exports.memory.buffer, arg, args[i].length) 125 | ) 126 | memory_data_view().setUint8(arg + args[i].length, 0) 127 | memory_data_view().setUint32(argv + 4 * i, arg, true) 128 | } 129 | memory_data_view().setUint32(argv + 4 * args.length, 0, true) 130 | const argv_ptr = instance.exports.malloc(4) 131 | memory_data_view().setUint32(argv_ptr, argv, true) 132 | 133 | instance.exports.hs_init_with_rtsopts(argc_ptr, argv_ptr) 134 | 135 | const args_ptr = instance.exports.malloc(args_str.length) 136 | new TextEncoder().encodeInto( 137 | args_str, 138 | new Uint8Array( 139 | instance.exports.memory.buffer, 140 | args_ptr, 141 | args_str.length 142 | ) 143 | ) 144 | 145 | inFile.data = await toUint8Array(inData) 146 | 147 | instance.exports.wasm_main(args_ptr, args_str.length) 148 | 149 | // Find any generated media files 150 | 151 | const knownFileNames = ["in", "out"].concat( 152 | resources.map(resource => resource.filename) 153 | ) 154 | const mediaFiles = new Map( 155 | [...rootDir.dir.contents] 156 | .filter(([name, _value]) => !knownFileNames.includes(name)) 157 | .map(([name, value]) => convertItem(name, value)) 158 | ) 159 | 160 | return { 161 | out: convertData(outFile.data), 162 | mediaFiles 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "formatter": { 3 | "enabled": true, 4 | "useEditorconfig": true, 5 | "formatWithErrors": false, 6 | "indentStyle": "space", 7 | "indentWidth": 4, 8 | "lineEnding": "lf", 9 | "lineWidth": 80, 10 | "attributePosition": "auto", 11 | "bracketSpacing": true, 12 | "ignore": [ 13 | "**/.transpile-cache/", 14 | "**/venv/", 15 | "**/static-transpile/", 16 | "**/static-libs/", 17 | "**/static-collected/", 18 | "**/node-modules/", 19 | "**/testing/", 20 | "**/.eslintrc.mjs", 21 | "**/*.json", 22 | "**/*.html", 23 | "**/.direnv", 24 | "**/venv", 25 | "**/.transpile", 26 | "**/.babelrc", 27 | "**/sw-template.js" 28 | ] 29 | }, 30 | "linter": { 31 | "enabled": true, 32 | "rules": { 33 | "recommended": false, 34 | "complexity": { 35 | "noExtraBooleanCast": "error", 36 | "noMultipleSpacesInRegularExpressionLiterals": "error", 37 | "noUselessCatch": "off", 38 | "noUselessConstructor": "off", 39 | "noUselessLabel": "error", 40 | "noUselessLoneBlockStatements": "error", 41 | "noUselessRename": "error", 42 | "noUselessStringConcat": "error", 43 | "noUselessTernary": "off", 44 | "noUselessUndefinedInitialization": "error", 45 | "noVoid": "error", 46 | "noWith": "error", 47 | "useLiteralKeys": "off" 48 | }, 49 | "correctness": { 50 | "noConstAssign": "error", 51 | "noConstantCondition": "error", 52 | "noEmptyCharacterClassInRegex": "error", 53 | "noEmptyPattern": "error", 54 | "noGlobalObjectCalls": "error", 55 | "noInnerDeclarations": "error", 56 | "noInvalidConstructorSuper": "error", 57 | "noInvalidUseBeforeDeclaration": "off", 58 | "noNewSymbol": "error", 59 | "noNonoctalDecimalEscape": "error", 60 | "noPrecisionLoss": "error", 61 | "noSelfAssign": "error", 62 | "noSetterReturn": "error", 63 | "noSwitchDeclarations": "error", 64 | "noUndeclaredVariables": "error", 65 | "noUnreachable": "error", 66 | "noUnreachableSuper": "error", 67 | "noUnsafeFinally": "error", 68 | "noUnsafeOptionalChaining": "error", 69 | "noUnusedLabels": "error", 70 | "noUnusedVariables": "error", 71 | "useArrayLiterals": "error", 72 | "useIsNan": "error", 73 | "useValidForDirection": "error", 74 | "useYield": "error" 75 | }, 76 | "security": { "noGlobalEval": "error" }, 77 | "style": { 78 | "noArguments": "off", 79 | "noCommaOperator": "error", 80 | "noNegationElse": "off", 81 | "noParameterAssign": "off", 82 | "noRestrictedGlobals": { "level": "error", "options": {} }, 83 | "noVar": "error", 84 | "noYodaExpression": "off", 85 | "useBlockStatements": "error", 86 | "useCollapsedElseIf": "off", 87 | "useConsistentBuiltinInstantiation": "error", 88 | "useConst": "warn", 89 | "useDefaultSwitchClause": "off", 90 | "useNumericLiterals": "error", 91 | "useShorthandAssign": "off", 92 | "useSingleVarDeclarator": "off", 93 | "useTemplate": "off" 94 | }, 95 | "suspicious": { 96 | "noAsyncPromiseExecutor": "error", 97 | "noCatchAssign": "error", 98 | "noClassAssign": "error", 99 | "noCompareNegZero": "error", 100 | "noControlCharactersInRegex": "off", 101 | "noDebugger": "error", 102 | "noDoubleEquals": "off", 103 | "noDuplicateCase": "error", 104 | "noDuplicateClassMembers": "error", 105 | "noDuplicateObjectKeys": "error", 106 | "noDuplicateParameters": "error", 107 | "noEmptyBlockStatements": "off", 108 | "noFallthroughSwitchClause": "error", 109 | "noFunctionAssign": "error", 110 | "noGlobalAssign": "error", 111 | "noImportAssign": "error", 112 | "noLabelVar": "error", 113 | "noMisleadingCharacterClass": "error", 114 | "noPrototypeBuiltins": "off", 115 | "noRedeclare": "error", 116 | "noSelfCompare": "error", 117 | "noShadowRestrictedNames": "error", 118 | "noSparseArray": "error", 119 | "noUnsafeNegation": "error", 120 | "useAwait": "error", 121 | "useGetterReturn": "error", 122 | "useValidTypeof": "error" 123 | } 124 | }, 125 | "ignore": [ 126 | "**/.transpile-cache/", 127 | "**/venv/", 128 | "**/static-transpile/", 129 | "**/static-libs/", 130 | "**/static-collected/", 131 | "**/testing/", 132 | "**/manifest.json", 133 | "**/sw-template.js" 134 | ] 135 | }, 136 | "javascript": { 137 | "formatter": { 138 | "jsxQuoteStyle": "double", 139 | "quoteProperties": "asNeeded", 140 | "trailingCommas": "none", 141 | "semicolons": "asNeeded", 142 | "indentWidth": 4, 143 | "arrowParentheses": "asNeeded", 144 | "bracketSameLine": true, 145 | "quoteStyle": "double", 146 | "attributePosition": "auto", 147 | "bracketSpacing": false 148 | } 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /patch/pandoc.patch: -------------------------------------------------------------------------------- 1 | diff --git a/cabal.project b/cabal.project 2 | index 4ca6da52e630..b3a4ffcbb87b 100644 3 | --- a/cabal.project 4 | +++ b/cabal.project 5 | @@ -2,9 +2,146 @@ 6 | pandoc-lua-engine 7 | pandoc-server 8 | pandoc-cli 9 | -tests: True 10 | -flags: +embed_data_files 11 | +tests: False 12 | constraints: skylighting-format-blaze-html >= 0.1.1.3, 13 | skylighting-format-context >= 0.1.0.2, 14 | -- for now (commercialhaskell/stackage#7545): 15 | data-default-class <= 0.2, data-default <= 0.8 16 | + 17 | +allow-newer: all:zlib 18 | + 19 | +package aeson 20 | + flags: -ordered-keymap 21 | + 22 | +package crypton 23 | + ghc-options: -optc-DARGON2_NO_THREADS 24 | + 25 | +package digest 26 | + flags: -pkg-config 27 | + 28 | +package pandoc 29 | + flags: +embed_data_files 30 | + 31 | +package pandoc-cli 32 | + flags: -lua -server 33 | + 34 | +allow-newer: 35 | + all:Cabal, 36 | + all:Cabal-syntax, 37 | + all:array, 38 | + all:base, 39 | + all:binary, 40 | + all:bytestring, 41 | + all:containers, 42 | + all:deepseq, 43 | + all:directory, 44 | + all:exceptions, 45 | + all:filepath, 46 | + all:ghc, 47 | + all:ghc-bignum, 48 | + all:ghc-boot, 49 | + all:ghc-boot-th, 50 | + all:ghc-compact, 51 | + all:ghc-experimental, 52 | + all:ghc-heap, 53 | + all:ghc-internal, 54 | + all:ghc-platform, 55 | + all:ghc-prim, 56 | + all:ghc-toolchain, 57 | + all:ghci, 58 | + all:haskeline, 59 | + all:hpc, 60 | + all:integer-gmp, 61 | + all:mtl, 62 | + all:os-string, 63 | + all:parsec, 64 | + all:pretty, 65 | + all:process, 66 | + all:rts, 67 | + all:semaphore-compat, 68 | + all:stm, 69 | + all:system-cxx-std-lib, 70 | + all:template-haskell, 71 | + all:text, 72 | + all:time, 73 | + all:transformers, 74 | + all:unix, 75 | + all:xhtml 76 | + 77 | +constraints: 78 | + Cabal installed, 79 | + Cabal-syntax installed, 80 | + array installed, 81 | + base installed, 82 | + binary installed, 83 | + bytestring installed, 84 | + containers installed, 85 | + deepseq installed, 86 | + directory installed, 87 | + exceptions installed, 88 | + filepath installed, 89 | + ghc installed, 90 | + ghc-bignum installed, 91 | + ghc-boot installed, 92 | + ghc-boot-th installed, 93 | + ghc-compact installed, 94 | + ghc-experimental installed, 95 | + ghc-heap installed, 96 | + ghc-internal installed, 97 | + ghc-platform installed, 98 | + ghc-prim installed, 99 | + ghc-toolchain installed, 100 | + ghci installed, 101 | + haskeline installed, 102 | + hpc installed, 103 | + integer-gmp installed, 104 | + mtl installed, 105 | + os-string installed, 106 | + parsec installed, 107 | + pretty installed, 108 | + process installed, 109 | + rts installed, 110 | + semaphore-compat installed, 111 | + stm installed, 112 | + system-cxx-std-lib installed, 113 | + template-haskell installed, 114 | + text installed, 115 | + time installed, 116 | + transformers installed, 117 | + unix installed, 118 | + xhtml installed 119 | + 120 | +-- https://github.com/haskell/network/pull/598 121 | +source-repository-package 122 | + type: git 123 | + location: https://github.com/haskell-wasm/network.git 124 | + tag: ab92e48e9fdf3abe214f85fdbe5301c1280e14e9 125 | + 126 | +source-repository-package 127 | + type: git 128 | + location: https://github.com/haskell-wasm/foundation.git 129 | + tag: 8e6dd48527fb429c1922083a5030ef88e3d58dd3 130 | + subdir: basement 131 | + 132 | +source-repository-package 133 | + type: git 134 | + location: https://github.com/haskell-wasm/hs-memory.git 135 | + tag: a198a76c584dc2cfdcde6b431968de92a5fed65e 136 | + 137 | +source-repository-package 138 | + type: git 139 | + location: https://github.com/haskell-wasm/xml.git 140 | + tag: bc793dc9bc29c92245d3482a54d326abd3ae1403 141 | + subdir: xml-conduit 142 | + 143 | +-- https://github.com/haskellari/splitmix/pull/73 144 | +source-repository-package 145 | + type: git 146 | + location: https://github.com/amesgen/splitmix 147 | + tag: 5f5b766d97dc735ac228215d240a3bb90bc2ff75 148 | + 149 | +source-repository-package 150 | + type: git 151 | + location: https://github.com/amesgen/cborg 152 | + tag: c3b5c696f62d04c0d87f55250bfc0016ab94d800 153 | + subdir: cborg 154 | diff --git a/pandoc-cli/pandoc-cli.cabal b/pandoc-cli/pandoc-cli.cabal 155 | index 5b904b9906bd..66d92a1875f3 100644 156 | --- a/pandoc-cli/pandoc-cli.cabal 157 | +++ b/pandoc-cli/pandoc-cli.cabal 158 | @@ -61,7 +61,7 @@ common common-options 159 | 160 | common common-executable 161 | import: common-options 162 | - ghc-options: -rtsopts -with-rtsopts=-A8m -threaded 163 | + ghc-options: -rtsopts -with-rtsopts=-H64m 164 | 165 | executable pandoc 166 | import: common-executable 167 | @@ -74,6 +74,10 @@ executable pandoc 168 | text 169 | other-modules: PandocCLI.Lua 170 | , PandocCLI.Server 171 | + 172 | + if arch(wasm32) 173 | + ghc-options: -optl-Wl,--export=__wasm_call_ctors,--export=hs_init_with_rtsopts,--export=malloc,--export=wasm_main 174 | + 175 | if flag(nightly) 176 | cpp-options: -DNIGHTLY 177 | build-depends: template-haskell, 178 | diff --git a/pandoc-cli/src/pandoc.hs b/pandoc-cli/src/pandoc.hs 179 | index 019d0adedb15..520a858c89a2 100644 180 | --- a/pandoc-cli/src/pandoc.hs 181 | +++ b/pandoc-cli/src/pandoc.hs 182 | @@ -1,5 +1,7 @@ 183 | {-# LANGUAGE CPP #-} 184 | +{-# LANGUAGE ScopedTypeVariables #-} 185 | {-# LANGUAGE TemplateHaskell #-} 186 | + 187 | {- | 188 | Module : Main 189 | Copyright : Copyright (C) 2006-2024 John MacFarlane 190 | @@ -34,6 +36,13 @@ import qualified Language.Haskell.TH as TH 191 | import Data.Time 192 | #endif 193 | 194 | +#if defined(wasm32_HOST_ARCH) 195 | +import Control.Exception 196 | +import Foreign 197 | +import Foreign.C 198 | +import System.IO 199 | +#endif 200 | + 201 | #ifdef NIGHTLY 202 | versionSuffix :: String 203 | versionSuffix = "-nightly-" ++ 204 | @@ -44,6 +53,24 @@ versionSuffix :: String 205 | versionSuffix = "" 206 | #endif 207 | 208 | +#if defined(wasm32_HOST_ARCH) 209 | + 210 | +foreign export ccall "wasm_main" wasm_main :: Ptr CChar -> Int -> IO () 211 | + 212 | +wasm_main :: Ptr CChar -> Int -> IO () 213 | +wasm_main raw_args_ptr raw_args_len = 214 | + catch act (\(err :: SomeException) -> hPrint stderr err) 215 | + where 216 | + act = do 217 | + args <- words <$> peekCStringLen (raw_args_ptr, raw_args_len) 218 | + free raw_args_ptr 219 | + engine <- getEngine 220 | + res <- parseOptionsFromArgs options defaultOpts "pandoc.wasm" $ args <> ["/in", "-o", "/out"] 221 | + case res of 222 | + Left e -> handleOptInfo engine e 223 | + Right opts -> convertWithOpts engine opts 224 | +#endif 225 | + 226 | main :: IO () 227 | main = E.handle (handleError . Left) $ do 228 | prg <- getProgName 229 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build, deploy, and release 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | jobs: 8 | pre-commit: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout code 12 | uses: actions/checkout@v4 13 | - name: Run pre-commit 14 | uses: pre-commit/action@v3.0.1 15 | prepare: 16 | runs-on: ubuntu-latest 17 | outputs: 18 | version: ${{ steps.extract-version.outputs.version }} 19 | pandoc_version: ${{ steps.extract-version.outputs.pandoc_version }} 20 | wasm_cache_key: ${{ steps.extract-version.outputs.wasm_cache_key }} 21 | wasm_cache_hit: ${{ steps.cache-wasm.outputs.cache-hit }} 22 | steps: 23 | - name: Checkout code 24 | uses: actions/checkout@v4 25 | 26 | - name: Extract versions for ${{ github.ref }} 27 | id: extract-version 28 | run: | 29 | VERSION=$(jq -r .version package.json) 30 | PANDOC_VERSION=$(cat pandoc-version.txt) 31 | { 32 | echo "version=${VERSION}"; 33 | echo "pandoc_version=${PANDOC_VERSION}"; 34 | echo "wasm_cache_key=wasm-${PANDOC_VERSION}-${{ hashFiles('patch/pandoc.patch') }}"; 35 | } >> "$GITHUB_OUTPUT" 36 | # Check if we already have the optimized WASM in cache 37 | - name: Check WASM cache 38 | id: cache-wasm 39 | uses: actions/cache/restore@v4 40 | with: 41 | path: dist/pandoc.wasm 42 | key: ${{ steps.extract-version.outputs.wasm_cache_key }} 43 | 44 | build-wasm: 45 | needs: prepare 46 | if: needs.prepare.outputs.wasm_cache_hit != 'true' 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Checkout code 50 | uses: actions/checkout@v4 51 | 52 | # Cache Haskell tools 53 | - name: Cache Haskell tools 54 | id: cache-tools 55 | uses: actions/cache@v4 56 | with: 57 | path: | 58 | ~/.cabal 59 | ~/.ghc-wasm 60 | key: haskell-tools-${{ needs.prepare.outputs.pandoc_version }} 61 | restore-keys: | 62 | haskell-tools- 63 | 64 | - name: Setup build tools 65 | if: steps.cache-tools.outputs.cache-hit != 'true' 66 | run: | 67 | temp_dir=$(mktemp -d) 68 | pushd "$temp_dir" 69 | cabal update 70 | cabal install alex happy 71 | echo "$HOME/.cabal/bin" >> "$GITHUB_PATH" 72 | popd 73 | 74 | - name: Setup GHC-WASM 75 | if: steps.cache-tools.outputs.cache-hit != 'true' 76 | run: | 77 | temp_dir=$(mktemp -d) 78 | pushd "$temp_dir" 79 | curl -f -L --retry 5 https://gitlab.haskell.org/haskell-wasm/ghc-wasm-meta/-/archive/92ff0eb8541eb0a6097922e3532c3fd44d2f7db4/ghc-wasm-meta-92ff0eb8541eb0a6097922e3532c3fd44d2f7db4.tar.gz | tar xz --strip-components=1 80 | FLAVOUR=9.12 ./setup.sh 81 | ~/.ghc-wasm/add_to_github_path.sh 82 | popd 83 | 84 | - name: Add cached tools to PATH 85 | if: steps.cache-tools.outputs.cache-hit == 'true' 86 | run: | 87 | echo "$HOME/.cabal/bin" >> "$GITHUB_PATH" 88 | ~/.ghc-wasm/add_to_github_path.sh 89 | 90 | - name: Checkout Pandoc 91 | uses: actions/checkout@v4 92 | with: 93 | repository: jgm/pandoc 94 | ref: ${{ needs.prepare.outputs.pandoc_version }} 95 | path: pandoc 96 | 97 | - name: Patch Pandoc 98 | run: | 99 | pushd pandoc 100 | patch -p1 < ../patch/pandoc.patch 101 | popd 102 | 103 | - name: Generate Cabal plan 104 | run: | 105 | pushd pandoc 106 | wasm32-wasi-cabal build pandoc-cli --dry-run 107 | popd 108 | 109 | # Cache Cabal dependencies and build artifacts 110 | - name: Cache Cabal dependencies 111 | uses: actions/cache@v4 112 | with: 113 | path: | 114 | ~/.ghc-wasm/.cabal/store 115 | pandoc/dist-newstyle 116 | key: wasm-cabal-cache-${{ needs.prepare.outputs.pandoc_version }}-${{ hashFiles('pandoc/dist-newstyle/cache/plan.json') }} 117 | restore-keys: | 118 | wasm-cabal-cache-${{ needs.prepare.outputs.pandoc_version }}- 119 | wasm-cabal-cache- 120 | 121 | - name: Build Pandoc WASM 122 | run: | 123 | pushd pandoc 124 | wasm32-wasi-cabal build pandoc-cli 125 | popd 126 | 127 | - name: Optimize WASM 128 | run: | 129 | mkdir -p dist 130 | WASM_PATH=$(find pandoc -name pandoc.wasm -type f) 131 | wasm-opt --low-memory-unused --converge --gufa --flatten --rereloop -Oz "$WASM_PATH" -o dist/pandoc.wasm 132 | cp src/*.js dist/ 133 | 134 | - name: Test build 135 | run: | 136 | wasmtime run --dir "$PWD"::/ -- dist/pandoc.wasm pandoc/README.md -o pandoc/README.rst 137 | head -20 pandoc/README.rst 138 | 139 | - name: Save to cache 140 | uses: actions/cache/save@v4 141 | with: 142 | path: dist/pandoc.wasm 143 | key: ${{ needs.prepare.outputs.wasm_cache_key }} 144 | 145 | - name: Upload artifact 146 | uses: actions/upload-artifact@v4 147 | with: 148 | name: wasm-build 149 | path: dist 150 | 151 | post-process: 152 | needs: [prepare, build-wasm] 153 | if: always() # Run even if build-wasm is skipped 154 | runs-on: ubuntu-latest 155 | steps: 156 | - name: Checkout code 157 | uses: actions/checkout@v4 158 | 159 | # Either get the artifact from build-wasm or from cache 160 | - name: Download built artifact 161 | if: needs.prepare.outputs.wasm_cache_hit != 'true' 162 | uses: actions/download-artifact@v4 163 | with: 164 | name: wasm-build 165 | path: dist 166 | 167 | # This step only runs if we hit the cache 168 | - name: Restore from cache 169 | if: needs.prepare.outputs.wasm_cache_hit == 'true' 170 | uses: actions/cache/restore@v4 171 | with: 172 | path: dist/pandoc.wasm 173 | key: ${{ needs.prepare.outputs.wasm_cache_key }} 174 | fail-on-cache-miss: true 175 | 176 | # Combine with JS files - they're not part of the cache key 177 | # but we need them in the artifact 178 | - name: Ensure JS files are included 179 | run: | 180 | cp src/*.js dist/ 181 | 182 | - name: Upload final artifact 183 | uses: actions/upload-artifact@v4 184 | with: 185 | name: wasm-pandoc-${{ needs.prepare.outputs.version }} 186 | path: dist 187 | 188 | deploy-pages: 189 | needs: [prepare, post-process] 190 | if: always() && !startsWith(github.ref, 'refs/tags/') 191 | runs-on: ubuntu-latest 192 | permissions: 193 | pages: write 194 | id-token: write 195 | steps: 196 | - name: Checkout repo 197 | uses: actions/checkout@v4 198 | 199 | - name: Download artifact 200 | uses: actions/download-artifact@v4 201 | with: 202 | name: wasm-pandoc-${{ needs.prepare.outputs.version }} 203 | path: dist 204 | 205 | - name: Prepare demo 206 | run: | 207 | cp dist/pandoc.wasm demo/ 208 | 209 | - name: Upload pages artifact 210 | uses: actions/upload-pages-artifact@v3 211 | with: 212 | path: demo 213 | 214 | - name: Deploy to Pages 215 | uses: actions/deploy-pages@v4 216 | 217 | release: 218 | needs: [prepare, post-process] 219 | if: always() && startsWith(github.ref, 'refs/tags/') 220 | runs-on: ubuntu-latest 221 | permissions: 222 | contents: write 223 | steps: 224 | - name: Checkout repo 225 | uses: actions/checkout@v4 226 | 227 | - name: Download artifact 228 | uses: actions/download-artifact@v4 229 | with: 230 | name: wasm-pandoc-${{ needs.prepare.outputs.version }} 231 | path: dist 232 | 233 | - name: Add metadata files 234 | run: cp {package.json,README.md,LICENSE} dist/ 235 | 236 | - name: Create release package 237 | run: | 238 | pushd dist 239 | zip -r ../wasm-pandoc-${{ needs.prepare.outputs.version }}.zip . 240 | popd 241 | 242 | - name: Upload release asset 243 | uses: softprops/action-gh-release@v2 244 | with: 245 | files: wasm-pandoc-${{ needs.prepare.outputs.version }}.zip 246 | token: ${{ secrets.GITHUB_TOKEN }} 247 | 248 | - name: Setup Node 249 | uses: actions/setup-node@v4 250 | with: 251 | node-version: "22" 252 | registry-url: "https://registry.npmjs.org" 253 | 254 | - name: Publish to NPM 255 | run: | 256 | cd dist 257 | npm publish 258 | env: 259 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 260 | --------------------------------------------------------------------------------