diff --git a/pandoc-cli/pandoc-cli.cabal b/pandoc-cli/pandoc-cli.cabal index ecfa91185..29549b952 100644 --- a/pandoc-cli/pandoc-cli.cabal +++ b/pandoc-cli/pandoc-cli.cabal @@ -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 diff --git a/pandoc-cli/src/pandoc-wasm.hs b/pandoc-cli/wasm/pandoc.hs similarity index 61% rename from pandoc-cli/src/pandoc-wasm.hs rename to pandoc-cli/wasm/pandoc.hs index 2fd3a00f9..0372a9197 100644 --- a/pandoc-cli/src/pandoc-wasm.hs +++ b/pandoc-cli/wasm/pandoc.hs @@ -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 diff --git a/wasm/index.js b/wasm/index.js index 26e43ca99..4de040f95 100644 --- a/wasm/index.js +++ b/wasm/index.js @@ -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)); diff --git a/wasm/pandoc.js b/wasm/pandoc.js index 0d602c076..99bdc2ac8 100644 --- a/wasm/pandoc.js +++ b/wasm/pandoc.js @@ -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);