wasm: change interface.

We now provide two functions: convert and query.
extensions_for_format has been dropped, as query can do the
work.

convert is what pandoc used to be.

query takes a JSON object describing the information needed,
and returns information. Currently only "extensions-for-format"
is implemented.

Also: the use of multiple main-is causes problems with cabal.
Instead, we use pandoc.hs as main throughout, but change the
hs-src-dir for wasm.
This commit is contained in:
John MacFarlane
2026-02-02 11:58:18 +01:00
parent 648999286d
commit f91faefbeb
4 changed files with 54 additions and 30 deletions
+5 -6
View File
@@ -71,21 +71,20 @@ common common-executable
executable pandoc
import: common-executable
hs-source-dirs: src
main-is: pandoc.hs
buildable: True
-- Note: we always link to an exact version of pandoc, with the
-- same version as this package:
build-depends: pandoc == 3.8.3
build-depends: pandoc == 3.8.3, text
other-modules: PandocCLI.Lua
, PandocCLI.Server
if arch(wasm32)
main-is: pandoc-wasm.hs
hs-source-dirs: wasm
build-depends: aeson, containers, bytestring
ghc-options: -optl-Wl,--export=__wasm_call_ctors,--export=hs_init_with_rtsopts,--export=malloc,--export=convert,--export=get_extensions_for_format
ghc-options: -optl-Wl,--export=__wasm_call_ctors,--export=hs_init_with_rtsopts,--export=malloc,--export=convert,--export=query
else
main-is: pandoc.hs
build-depends: text
hs-source-dirs: src
if flag(nightly)
cpp-options: -DNIGHTLY
@@ -1,4 +1,5 @@
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : Main
@@ -25,13 +26,14 @@ import PandocCLI.Lua
import Control.Exception
import Foreign
import Foreign.C
import qualified Data.Aeson as Aeson
import Data.Aeson as Aeson
import qualified Text.Pandoc.UTF8 as UTF8
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T
foreign export ccall "convert" convert :: Ptr CChar -> Int -> IO ()
-- | The parameters are a pointer and length for a C string
-- | The parameters are a pointer and length for a C string
-- containing JSON-encoded pandoc options (isomorphic to a defaults
-- file). The calling program should set up a virtual file system
-- containing @/stdin@ (input), @/stdout@ (output), and @/warnings@ (output).
@@ -59,22 +61,41 @@ convert ptr len =
}
convertWithOpts engine opts'
foreign export ccall "get_extensions_for_format" get_extensions_for_format
:: Ptr CChar -> Int -> IO ()
foreign export ccall "query" query :: Ptr CChar -> Int -> IO ()
-- | Given a C string with the format name, returnsa JSON object
-- mapping extensions relevant for this format to true or false,
-- depending on whether they are enabled by default.
get_extensions_for_format :: Ptr CChar -> Int -> IO ()
get_extensions_for_format ptr len = do
formatName <- readMaybe <$> getCString ptr len
case formatName of
Just fmt -> do
let allExts = getAllExtensions fmt
let defExts = getDefaultExtensions fmt
let addExt x = M.insert (drop 4 (show x)) (extensionEnabled x defExts)
BL.writeFile "/stdout" $ Aeson.encode $ foldr addExt mempty (extensionsToList allExts)
Nothing -> writeFile "/stdout" "{}"
-- | The parameters are a pointer and length for a C string
-- containing a JSON-encoded query, e.g.,
-- @{ "query": "extensions-for-format", "argument": "markdown" }@.
-- The calling program should set up a virtual file system
-- containing @/stdout@, which will be used to hold the output,
-- and @/stderr@, which will be used to hold any error messages.
query :: Ptr CChar -> Int -> IO ()
query ptr len =
E.catch act (\(err :: SomeException) ->
writeFile "/stderr" ("ERROR: " <> displayException err))
where
act = do
args <- getCString ptr len
case Aeson.eitherDecode (UTF8.fromStringLazy args) of
Left e -> error e
Right (ExtensionsForFormat fmt) -> do
let allExts = getAllExtensions fmt
let defExts = getDefaultExtensions fmt
let addExt x = M.insert (drop 4 (show x))
(extensionEnabled x defExts)
BL.writeFile "/stdout" $ Aeson.encode $
foldr addExt mempty (extensionsToList allExts)
data Query =
ExtensionsForFormat T.Text
deriving (Show)
instance FromJSON Query where
parseJSON = withObject "Query" $ \o -> do
queryType <- o .: "query"
case queryType of
"extensions-for-format" -> ExtensionsForFormat <$> o .: "format"
_ -> fail $ "Unknown query type " <> queryType
getCString :: Ptr CChar -> Int -> IO String
getCString ptr len = peekCStringLen (ptr, len) <* free ptr
+4 -4
View File
@@ -1,11 +1,11 @@
import { zipSync, unzipSync, strToU8, strFromU8 } from 'https://esm.sh/fflate@0.8.2';
import { convert, getExtensionsForFormat } from "./pandoc.js?sha1=SHA1_PANDOC_JS";
import { convert, query } from "./pandoc.js?sha1=SHA1_PANDOC_JS";
// Make fflate available globally
window.fflate = { zipSync, unzipSync, strToU8, strFromU8 };
// Make pandoc available globally for the app
window.pandocModule = { convert, getExtensionsForFormat };
window.pandocModule = { convert, query };
// Lazy-load typst library only when needed
let typstLoaded = false;
@@ -1014,7 +1014,7 @@ window.pandocApp = function() {
if (this.inputFormat !== 'auto') {
try {
const extData = await window.pandocModule.getExtensionsForFormat(this.inputFormat);
const extData = await window.pandocModule.query({ query: "extensions-for-format", format: this.inputFormat });
this.inputExtensionsList = Object.entries(extData)
.map(([name, defaultOn]) => ({ name, defaultOn }))
.sort((a, b) => a.name.localeCompare(b.name));
@@ -1024,7 +1024,7 @@ window.pandocApp = function() {
const outFmt = this.outputFormat === 'pdf-typst' ? 'typst' : this.outputFormat;
if (outFmt !== 'auto') {
try {
const extData = await window.pandocModule.getExtensionsForFormat(outFmt);
const extData = await window.pandocModule.query({ query: "extensions-for-format", format: outFmt});
this.outputExtensionsList = Object.entries(extData)
.map(([name, defaultOn]) => ({ name, defaultOn }))
.sort((a, b) => a.name.localeCompare(b.name));
+7 -3
View File
@@ -70,7 +70,7 @@ memory_data_view().setUint32(argv_ptr, argv, true);
instance.exports.hs_init_with_rtsopts(argc_ptr, argv_ptr);
export async function getExtensionsForFormat(options) {
export async function query(options) {
const opts_str = JSON.stringify(options);
const opts_ptr = instance.exports.malloc(opts_str.length);
new TextEncoder().encodeInto(
@@ -83,11 +83,15 @@ export async function getExtensionsForFormat(options) {
const err_file = new File(new Uint8Array(), { readonly: false });
fileSystem.set("stdout", out_file);
fileSystem.set("stderr", err_file);
instance.exports.get_extensions_for_format(opts_ptr, opts_str.length);
instance.exports.query(opts_ptr, opts_str.length);
return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(out_file.data));
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_ptr = instance.exports.malloc(opts_str.length);