├── .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 |
--------------------------------------------------------------------------------