aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile13
-rw-r--r--cabal.project89
-rw-r--r--flake.lock79
-rw-r--r--flake.nix11
-rw-r--r--pandoc-cli/lua/PandocCLI/Lua.hs15
-rw-r--r--pandoc-cli/pandoc-cli.cabal32
-rw-r--r--pandoc-cli/src/pandoc-wasm.hs74
-rw-r--r--pandoc-cli/src/pandoc.hs4
-rw-r--r--pandoc-lua-engine/pandoc-lua-engine.cabal8
-rw-r--r--pandoc-lua-engine/src/Text/Pandoc/Lua/Module/CLI.hs10
-rw-r--r--wasm/LICENSE21
-rw-r--r--wasm/Makefile34
-rw-r--r--wasm/examples/bibtex-to-csljson/options.json4
-rw-r--r--wasm/examples/bibtex-to-csljson/stdin12
-rw-r--r--wasm/examples/csv-table-to-org/options.json4
-rw-r--r--wasm/examples/csv-table-to-org/stdin10
-rw-r--r--wasm/examples/custom-template/custom.tpl6
-rw-r--r--wasm/examples/custom-template/options.json6
-rw-r--r--wasm/examples/custom-template/stdin10
-rw-r--r--wasm/examples/docx-with-equations-to-latex/equations.docxbin0 -> 10921 bytes
-rw-r--r--wasm/examples/docx-with-equations-to-latex/options.json6
-rw-r--r--wasm/examples/hello-world/options.json4
-rw-r--r--wasm/examples/hello-world/stdin1
-rw-r--r--wasm/examples/highlighted-code-to-html/options.json13
-rw-r--r--wasm/examples/highlighted-code-to-html/stdin39
-rw-r--r--wasm/examples/ipynb-to-rtf/options.json5
-rw-r--r--wasm/examples/ipynb-to-rtf/stdin68
-rw-r--r--wasm/examples/latex-to-docbook-with-mathml/options.json6
-rw-r--r--wasm/examples/latex-to-docbook-with-mathml/stdin27
-rw-r--r--wasm/examples/latex-with-macros-to-restructured-text/options.json6
-rw-r--r--wasm/examples/latex-with-macros-to-restructured-text/stdin9
-rw-r--r--wasm/examples/man-page-to-context/options.json4
-rw-r--r--wasm/examples/man-page-to-context/stdin40
-rw-r--r--wasm/examples/markdown-citations-to-plain-with-csl-style/le-tapuscrit-note.csl496
-rw-r--r--wasm/examples/markdown-citations-to-plain-with-csl-style/options.json7
-rw-r--r--wasm/examples/markdown-citations-to-plain-with-csl-style/refs.bib9
-rw-r--r--wasm/examples/markdown-citations-to-plain-with-csl-style/stdin7
-rw-r--r--wasm/examples/markdown-to-docbook-with-citations/options.json6
-rw-r--r--wasm/examples/markdown-to-docbook-with-citations/stdin22
-rw-r--r--wasm/examples/markdown-to-revealjs-slides/options.json9
-rw-r--r--wasm/examples/markdown-to-revealjs-slides/stdin560
-rw-r--r--wasm/examples/markdown-to-rst/options.json10
-rw-r--r--wasm/examples/markdown-to-rst/stdin250
-rw-r--r--wasm/examples/mediawiki-to-docx-with-equations/options.json6
-rw-r--r--wasm/examples/mediawiki-to-docx-with-equations/stdin8
-rw-r--r--wasm/examples/ris-to-formatted-markdown-bibliography/options.json9
-rw-r--r--wasm/examples/ris-to-formatted-markdown-bibliography/stdin20
-rw-r--r--wasm/examples/rst-table-from-yaml-data/custom.rst7
-rw-r--r--wasm/examples/rst-table-from-yaml-data/options.json8
-rw-r--r--wasm/examples/rst-table-from-yaml-data/species.rst2
-rw-r--r--wasm/examples/rst-table-from-yaml-data/stdin721
-rw-r--r--wasm/index.html2486
-rw-r--r--wasm/pandoc.js141
53 files changed, 5434 insertions, 20 deletions
diff --git a/Makefile b/Makefile
index b42183567..d64412ac0 100644
--- a/Makefile
+++ b/Makefile
@@ -17,6 +17,7 @@ WEBSITE=../../web/pandoc.org
REVISION?=1
BENCHARGS?=--csv bench_$(TIMESTAMP).csv $(BASELINECMD) --timeout=6 +RTS -T --nonmoving-gc -RTS $(if $(PATTERN),--pattern "$(PATTERN)",)
pandoc=$(shell cabal list-bin $(CABALOPTS) pandoc-cli)
+OPTIMIZE_WASM?=1
all: build test binpath ## build executable and run tests
.PHONY: all
@@ -326,3 +327,15 @@ release-checklist-$(VERSION).org: RELEASE-CHECKLIST-TEMPLATE.org
hie.yaml: ## regenerate hie.yaml
gen-hie > $@
.PHONY: hie.yaml
+
+pandoc.wasm:
+ -rm $@
+ wasm32-wasi-cabal build pandoc-cli
+ifeq ($(OPTIMIZE_WASM),1)
+ echo "Optimizing (this may take a long time, to avoid, set OPTIMIZE_WASM=0)..."
+ wasm-opt --low-memory-unused --converge --gufa --flatten --rereloop -Oz $$(wasm32-wasi-cabal list-bin pandoc-cli | tail -1) -o $@
+else
+ echo "Copying unoptimized pandoc.wasm..."
+ cp "$$(wasm32-wasi-cabal list-bin pandoc-cli | tail -1)" "$@"
+endif
+.PHONY: pandoc.wasm
diff --git a/cabal.project b/cabal.project
index 5629d8c17..c31f628c0 100644
--- a/cabal.project
+++ b/cabal.project
@@ -2,11 +2,12 @@ packages: .
pandoc-lua-engine
pandoc-server
pandoc-cli
-tests: True
-flags: +embed_data_files
constraints: skylighting-format-blaze-html >= 0.1.1.3,
skylighting-format-context >= 0.1.0.2
+package pandoc
+ flags: +embed_data_files +http
+
source-repository-package
type: git
location: https://github.com/jgm/asciidoc-hs.git
@@ -31,3 +32,87 @@ source-repository-package
type: git
location: https://github.com/jgm/djoths.git
tag: a6fc625fd58aa39df05dd1d82ee005e681815095
+
+if arch(wasm32)
+ tests: False
+
+ package atomic-counter
+ flags: +no-cmm
+
+ package aeson
+ flags: -ordered-keymap
+
+ package crypton
+ ghc-options: -optc-DARGON2_NO_THREADS
+
+ package digest
+ flags: -pkg-config
+
+ package lua
+ flags: +cross-compile
+ ghc-options: -optc-D_WASI_EMULATED_SIGNAL -optc-lwasi-emulated-signal -optc-mllvm -optc-wasm-enable-sjlj -optc-mllvm -optc-wasm-use-legacy-eh -optc-D_WASI_EMULATED_PROCESS_CLOCKS -optc-lwasi-emulated-process-clocks -optc-DLUA_NOTEMP -optl-lsetjmp
+ ld-options: -lsetjmp
+
+ package pandoc
+ flags: +embed_data_files -http
+
+ package pandoc-cli
+ flags: -lua -server
+
+ package pandoc-lua-engine
+ flags: -repl
+
+ package pandoc-cli
+ flags: -repl
+
+ allow-newer:
+ all:base,
+ all:binary,
+ all:bytestring,
+ all:containers,
+ all:ghc-bignum,
+ all:template-haskell,
+ all:text,
+ all:time
+
+ source-repository-package
+ type: git
+ location: https://github.com/haskell-wasm/conduit.git
+ tag: ff33329247f2ef321dcab836e98c1bcfaff2bd13
+ subdir: conduit-extra
+
+ source-repository-package
+ type: git
+ location: https://github.com/haskell-wasm/foundation.git
+ tag: 8e6dd48527fb429c1922083a5030ef88e3d58dd3
+ subdir: basement
+
+ source-repository-package
+ type: git
+ location: https://github.com/haskell-wasm/hs-memory.git
+ tag: a198a76c584dc2cfdcde6b431968de92a5fed65e
+
+ source-repository-package
+ type: git
+ location: https://github.com/haskell-wasm/streaming-commons.git
+ tag: 7e9c38b2fd55ce50d3f74fe708ca47db8c9bb315
+
+ source-repository-package
+ type: git
+ location: https://github.com/haskell-wasm/xml.git
+ tag: bc793dc9bc29c92245d3482a54d326abd3ae1403
+ subdir: xml-conduit
+
+ -- https://github.com/haskellari/splitmix/pull/73
+ source-repository-package
+ type: git
+ location: https://github.com/amesgen/splitmix
+ tag: 5f5b766d97dc735ac228215d240a3bb90bc2ff75
+
+ -- only needed if we have +lua
+ source-repository-package
+ type: git
+ location: https://github.com/jgm/hslua.git
+ subdir: lua
+ tag: 013a0751a29f810a1a954eee4f237441007772ef
+
diff --git a/flake.lock b/flake.lock
index 864c91efd..8b1d3d2f3 100644
--- a/flake.lock
+++ b/flake.lock
@@ -18,13 +18,68 @@
"type": "github"
}
},
+ "flake-utils_2": {
+ "inputs": {
+ "systems": "systems_2"
+ },
+ "locked": {
+ "lastModified": 1731533236,
+ "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
+ "type": "github"
+ },
+ "original": {
+ "owner": "numtide",
+ "repo": "flake-utils",
+ "type": "github"
+ }
+ },
+ "ghc-wasm-meta": {
+ "inputs": {
+ "flake-utils": "flake-utils_2",
+ "nixpkgs": "nixpkgs"
+ },
+ "locked": {
+ "host": "gitlab.haskell.org",
+ "lastModified": 1767359139,
+ "narHash": "sha256-Ttinox5tFOmaK3WRExsQfBFw6XT+8QwwrXQGdL96mN4=",
+ "owner": "haskell-wasm",
+ "repo": "ghc-wasm-meta",
+ "rev": "184c5936d3e51fdb21f9e9d8a0a0c88481767323",
+ "type": "gitlab"
+ },
+ "original": {
+ "host": "gitlab.haskell.org",
+ "owner": "haskell-wasm",
+ "repo": "ghc-wasm-meta",
+ "type": "gitlab"
+ }
+ },
"nixpkgs": {
"locked": {
- "lastModified": 1767606049,
- "narHash": "sha256-OE2fSzvBXKRe4K5/USQkdopiDU+5PdgJ8tIl5M6nY0s=",
+ "lastModified": 1767047869,
+ "narHash": "sha256-tzYsEzXEVa7op1LTnrLSiPGrcCY6948iD0EcNLWcmzo=",
+ "owner": "NixOS",
+ "repo": "nixpkgs",
+ "rev": "89dbf01df72eb5ebe3b24a86334b12c27d68016a",
+ "type": "github"
+ },
+ "original": {
+ "owner": "NixOS",
+ "ref": "nixos-25.11",
+ "repo": "nixpkgs",
+ "type": "github"
+ }
+ },
+ "nixpkgs_2": {
+ "locked": {
+ "lastModified": 1768089576,
+ "narHash": "sha256-x8tU88T91e1AEemjmaaLKkZsQMfsFyP9W0ri9o8ff7U=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "2259340fabfc9c61fd9961ef7e2520f77bfc08e9",
+ "rev": "aef8bcdfb3ec01e5650b67da9ce9bf25806cc47d",
"type": "github"
},
"original": {
@@ -36,7 +91,8 @@
"root": {
"inputs": {
"flake-utils": "flake-utils",
- "nixpkgs": "nixpkgs"
+ "ghc-wasm-meta": "ghc-wasm-meta",
+ "nixpkgs": "nixpkgs_2"
}
},
"systems": {
@@ -53,6 +109,21 @@
"repo": "default",
"type": "github"
}
+ },
+ "systems_2": {
+ "locked": {
+ "lastModified": 1681028828,
+ "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
+ "owner": "nix-systems",
+ "repo": "default",
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
+ "type": "github"
+ },
+ "original": {
+ "owner": "nix-systems",
+ "repo": "default",
+ "type": "github"
+ }
}
},
"root": "root",
diff --git a/flake.nix b/flake.nix
index f653bceab..23d057f0f 100644
--- a/flake.nix
+++ b/flake.nix
@@ -4,9 +4,11 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs";
flake-utils.url = "github:numtide/flake-utils";
+
+ ghc-wasm-meta.url = "gitlab:haskell-wasm/ghc-wasm-meta?host=gitlab.haskell.org";
};
- outputs = { self, nixpkgs, flake-utils }:
+ outputs = { self, nixpkgs, flake-utils, ghc-wasm-meta }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
@@ -33,6 +35,10 @@
# DON'T FORGET TO PUT YOUR PACKAGE NAME HERE, REMOVING `throw`
packageName = "pandoc";
+
+ wasmToolchain = ghc-wasm-meta.packages.${system}.default;
+ # Alternatively, if you want a specific "bundle" exposed by ghc-wasm-meta:
+ # wasmToolchain = ghc-wasm-meta.packages.${system}.all_9_14;
in {
packages.${packageName} =
haskellPackages.callCabal2nix packageName self rec {
@@ -44,6 +50,8 @@
devShells.default = pkgs.mkShell {
buildInputs = with pkgs; [
+ wasmToolchain
+
haskellPackages.haskell-language-server # you must build it with your ghc to work
haskellPackages.hlint
haskellPackages.cabal-install
@@ -66,6 +74,7 @@
];
inputsFrom = map (__getAttr "env") (__attrValues self.packages.${system});
};
+
devShell = self.devShells.${system}.default;
});
}
diff --git a/pandoc-cli/lua/PandocCLI/Lua.hs b/pandoc-cli/lua/PandocCLI/Lua.hs
index df00963dd..d13e2b0a3 100644
--- a/pandoc-cli/lua/PandocCLI/Lua.hs
+++ b/pandoc-cli/lua/PandocCLI/Lua.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{- |
Module : PandocCLI.Lua
@@ -9,8 +10,8 @@ Functions to run the pandoc Lua scripting engine.
-}
module PandocCLI.Lua (runLuaInterpreter, getEngine) where
+#ifdef REPL
import Control.Monad ((<=<))
-import HsLua.CLI (EnvBehavior (..), Settings (..), runStandalone)
import System.Environment (lookupEnv)
import System.IO.Temp (withSystemTempFile)
import System.IO (hClose)
@@ -18,6 +19,12 @@ import Text.Pandoc.Class (runIOorExplode)
import Text.Pandoc.Error (handleError)
import Text.Pandoc.Lua (runLua, runLuaNoEnv, getEngine)
import Text.Pandoc.Version (pandocVersionText)
+import HsLua.CLI (EnvBehavior (..), Settings (..), runStandalone)
+#else
+import Text.Pandoc.Lua (getEngine)
+import System.IO (stderr, hPutStrLn)
+import System.Exit (exitWith, ExitCode(..))
+#endif
-- | Runs pandoc as a Lua interpreter that is (mostly) compatible with
-- the default @lua@ program shipping with Lua.
@@ -28,6 +35,7 @@ import Text.Pandoc.Version (pandocVersionText)
runLuaInterpreter :: String -- ^ Program name
-> [String] -- ^ Command line arguments
-> IO ()
+#ifdef REPL
runLuaInterpreter progName args = do
-- We need some kind of temp
mbhistfile <- lookupEnv "PANDOC_REPL_HISTORY"
@@ -54,3 +62,8 @@ runLuaInterpreter progName args = do
IgnoreEnvVars -> runLuaNoEnv
ConsultEnvVars -> runLua
in handleError <=< runIOorExplode . runLua'
+#else
+runLuaInterpreter _ _ = do
+ hPutStrLn stderr "Pandoc not compiled with Lua interpreter support."
+ exitWith $ ExitFailure 4
+#endif
diff --git a/pandoc-cli/pandoc-cli.cabal b/pandoc-cli/pandoc-cli.cabal
index c39c5b2eb..8fce95672 100644
--- a/pandoc-cli/pandoc-cli.cabal
+++ b/pandoc-cli/pandoc-cli.cabal
@@ -14,11 +14,13 @@ category: Text
synopsis: Conversion between documentation formats
description: Pandoc-cli provides a command-line executable that uses the
pandoc library to convert between markup formats.
--- data-files:
extra-source-files:
man/pandoc.1
man/pandoc-lua.1
man/pandoc-server.1
+ wasm/pandoc.js
+ wasm/index.html
+ wasm/LICENSE
source-repository head
type: git
location: https://github.com/jgm/pandoc.git
@@ -32,6 +34,10 @@ flag server
Description: Include support for running pandoc as an HTTP server.
Default: True
+flag repl
+ Description: Include support for running a pandoc Lua repl.
+ Default: True
+
flag nightly
Description: Add '-nightly-COMPILEDATE' to the output of '--version'.
Default: False
@@ -61,19 +67,26 @@ common common-options
common common-executable
import: common-options
- ghc-options: -rtsopts -with-rtsopts=-A8m -threaded
+ ghc-options: -rtsopts -with-rtsopts=-H64m
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,
- text
+ build-depends: pandoc == 3.8.3
other-modules: PandocCLI.Lua
, PandocCLI.Server
+
+ if arch(wasm32)
+ main-is: pandoc-wasm.hs
+ build-depends: aeson, containers, bytestring
+ ghc-options: -optl-Wl,--export=__wasm_call_ctors,--export=hs_init_with_rtsopts,--export=malloc,--export=wasm_main,--export=get_extensions_for_format
+ else
+ main-is: pandoc.hs
+ build-depneds: text
+
if flag(nightly)
cpp-options: -DNIGHTLY
build-depends: template-haskell,
@@ -88,9 +101,12 @@ executable pandoc
hs-source-dirs: no-server
if flag(lua)
- build-depends: hslua-cli >= 1.4.1 && < 1.5,
- pandoc-lua-engine >= 0.5 && < 0.6,
- temporary >= 1.1 && < 1.4
+ build-depends: pandoc-lua-engine >= 0.5 && < 0.6
hs-source-dirs: lua
else
hs-source-dirs: no-lua
+
+ if flag(repl)
+ build-depends: hslua-cli >= 1.4.1 && < 1.5,
+ temporary >= 1.1 && < 1.4
+ cpp-options: -DREPL
diff --git a/pandoc-cli/src/pandoc-wasm.hs b/pandoc-cli/src/pandoc-wasm.hs
new file mode 100644
index 000000000..a8d7cc80a
--- /dev/null
+++ b/pandoc-cli/src/pandoc-wasm.hs
@@ -0,0 +1,74 @@
+{-# LANGUAGE ScopedTypeVariables #-}
+
+{- |
+ Module : Main
+ Copyright : Copyright (C) 2006-2024 John MacFarlane
+ License : GNU GPL, version 2 or above
+
+ Maintainer : John MacFarlane <jgm@berkeley@edu>
+ Stability : alpha
+ Portability : portable
+
+Parses command-line options and calls the appropriate readers and
+writers (wasm version).
+-}
+module Main where
+import qualified Data.Map as M
+import Text.Read (readMaybe)
+import qualified Control.Exception as E
+import Data.Maybe (fromMaybe)
+import Text.Pandoc.App ( convertWithOpts, Opt(..), defaultOpts )
+import Text.Pandoc (Verbosity(ERROR))
+import Text.Pandoc.Extensions (extensionsToList, extensionEnabled, getAllExtensions,
+ getDefaultExtensions)
+import PandocCLI.Lua
+import Control.Exception
+import Foreign
+import Foreign.C
+import qualified Data.Aeson as Aeson
+import qualified Text.Pandoc.UTF8 as UTF8
+import qualified Data.ByteString.Lazy as BL
+
+foreign export ccall "wasm_main" wasm_main :: Ptr CChar -> Int -> IO ()
+
+wasm_main :: Ptr CChar -> Int -> IO ()
+wasm_main raw_args_ptr raw_args_len =
+ E.catch act (\(err :: SomeException) ->
+ writeFile "/stderr" ("ERROR: " <> displayException err))
+ where
+ act = do
+ args <- peekCStringLen (raw_args_ptr, raw_args_len)
+ free raw_args_ptr
+ engine <- getEngine
+ let aesonRes = Aeson.eitherDecode (UTF8.fromStringLazy args)
+ case aesonRes of
+ Left e -> error e
+ Right (f :: Opt -> Opt) -> do
+ let opts = f defaultOpts
+ let opts' = opts{ optInputFiles =
+ Just $ fromMaybe ["/stdin"] (optInputFiles opts)
+ , optOutputFile =
+ Just $ fromMaybe "/stdout" (optOutputFile opts)
+ , optLogFile =
+ Just $ fromMaybe "/warnings" (optLogFile opts)
+ , optVerbosity = ERROR -- only show errors to stderr
+ }
+ convertWithOpts engine opts'
+
+foreign export ccall "get_extensions_for_format" getExtensionsForFormat :: Ptr CChar -> Int -> IO ()
+
+getExtensionsForFormat :: Ptr CChar -> Int -> IO ()
+getExtensionsForFormat raw_fmt_ptr raw_fmt_len = do
+ formatName <- readMaybe <$> peekCStringLen (raw_fmt_ptr, raw_fmt_len)
+ free raw_fmt_ptr
+ 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" "{}"
+
+-- This must be included or we get an error:
+main :: IO ()
+main = pure ()
diff --git a/pandoc-cli/src/pandoc.hs b/pandoc-cli/src/pandoc.hs
index d110c6f62..b6f41d1d6 100644
--- a/pandoc-cli/src/pandoc.hs
+++ b/pandoc-cli/src/pandoc.hs
@@ -1,5 +1,7 @@
{-# LANGUAGE CPP #-}
+{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
+
{- |
Module : Main
Copyright : Copyright (C) 2006-2024 John MacFarlane
@@ -23,7 +25,7 @@ import PandocCLI.Lua
import PandocCLI.Server
import Text.Pandoc.Scripting (ScriptingEngine(..))
import qualified Data.Text as T
-
+import System.Exit
#ifdef NIGHTLY
import qualified Language.Haskell.TH as TH
import Data.Time
diff --git a/pandoc-lua-engine/pandoc-lua-engine.cabal b/pandoc-lua-engine/pandoc-lua-engine.cabal
index cb35943f6..bc8543fe0 100644
--- a/pandoc-lua-engine/pandoc-lua-engine.cabal
+++ b/pandoc-lua-engine/pandoc-lua-engine.cabal
@@ -44,6 +44,10 @@ source-repository head
location: https://github.com/jgm/pandoc.git
subdir: pandoc-lua-engine
+flag repl
+ Description: Include pandoc Lua repl.
+ Default: True
+
common common-options
default-language: Haskell2010
build-depends: base >= 4.12 && < 5
@@ -125,7 +129,6 @@ library
, hslua-module-text >= 1.2 && < 1.3
, hslua-module-version >= 1.2 && < 1.3
, hslua-module-zip >= 1.1.5 && < 1.3
- , hslua-repl >= 0.1.1 && < 0.2
, lpeg >= 1.1 && < 1.2
, mtl >= 2.2 && < 2.4
, pandoc >= 3.8 && < 3.9
@@ -134,6 +137,9 @@ library
, parsec >= 3.1 && < 3.2
, text >= 1.1.1 && < 2.2
+ if flag(repl)
+ build-depends: hslua-repl >= 0.1.1 && < 0.2
+ cpp-options: -DREPL
test-suite test-pandoc-lua-engine
import: common-options
diff --git a/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/CLI.hs b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/CLI.hs
index 3d8ff2727..8bbfd67fe 100644
--- a/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/CLI.hs
+++ b/pandoc-lua-engine/src/Text/Pandoc/Lua/Module/CLI.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE CPP #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{- |
@@ -15,11 +16,13 @@ module Text.Pandoc.Lua.Module.CLI
import Control.Applicative ((<|>))
import Data.Version (makeVersion)
import HsLua
-import HsLua.REPL (defaultConfig, replWithEnv, setup)
import Text.Pandoc.App (defaultOpts, options, parseOptionsFromArgs)
import Text.Pandoc.Error (PandocError)
import Text.Pandoc.Lua.PandocLua ()
import qualified Data.Text as T
+#ifdef REPL
+import HsLua.REPL (defaultConfig, replWithEnv, setup)
+#endif
-- | Push the pandoc.types module on the Lua stack.
documentedModule :: Module PandocError
@@ -46,8 +49,9 @@ documentedModule = defmodule "pandoc.cli"
, "scripts, taking the list of arguments from the global `arg`."
]
`since` makeVersion [3, 0]
-
+#ifdef REPL
, repl `since` makeVersion [3, 1, 2]
+#endif
]
where
peekArgs idx =
@@ -61,6 +65,7 @@ documentedModule = defmodule "pandoc.cli"
Left e -> failLua $ "Cannot process info option: " ++ show e
Right opts -> pure opts
+#ifdef REPL
-- | Starts a REPL.
repl :: DocumentedFunction PandocError
repl = defun "repl"
@@ -117,3 +122,4 @@ repl = defun "repl"
copyval
copyval
pop 1 -- global table
+#endif
diff --git a/wasm/LICENSE b/wasm/LICENSE
new file mode 100644
index 000000000..a3d114ca9
--- /dev/null
+++ b/wasm/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) Tweag I/O Limited.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/wasm/Makefile b/wasm/Makefile
new file mode 100644
index 000000000..f84ef0e42
--- /dev/null
+++ b/wasm/Makefile
@@ -0,0 +1,34 @@
+SHA1=$(shell openssl sha1 -r pandoc.wasm | sed 's/ .*$$//')
+EXAMPLES=$(patsubst %,%.tar,$(shell find examples -mindepth 1 -type d))
+
+site/examples:
+ mkdir -p $@
+
+site: site/examples $(patsubst %,site/%,$(EXAMPLES))
+
+examples: $(EXAMPLES)
+ echo $(EXAMPLES)
+.PHONY: examples
+
+site/pandoc.js: pandoc.js
+ perl -p -e "s/pandoc.wasm\?sha1=XX/pandoc.wasm?sha1=$(SHA1)/" $< > $@
+
+site/%.html: %.html
+ perl -p -e "s/pandoc.js\?sha1=XX/pandoc.js?sha1=$(SHA1)/" $< > $@
+
+site/examples/%.tar: examples/%.tar
+ cp $< $@
+
+site/%: %
+ cp $< $@
+
+upload: site site/pandoc.js site/index.html site/pandoc.wasm
+ rsync -av site/ website:pandoc.org/wasm-demo
+.PHONY: upload
+
+examples/%.tar: examples/%
+ tar -cf "$@" -C "$<" .
+
+serve:
+ python3 -m http.server
+.PHONY: serve
diff --git a/wasm/examples/bibtex-to-csljson/options.json b/wasm/examples/bibtex-to-csljson/options.json
new file mode 100644
index 000000000..d84da5605
--- /dev/null
+++ b/wasm/examples/bibtex-to-csljson/options.json
@@ -0,0 +1,4 @@
+{
+ "from": "bibtex",
+ "to": "csljson"
+} \ No newline at end of file
diff --git a/wasm/examples/bibtex-to-csljson/stdin b/wasm/examples/bibtex-to-csljson/stdin
new file mode 100644
index 000000000..e36e5b491
--- /dev/null
+++ b/wasm/examples/bibtex-to-csljson/stdin
@@ -0,0 +1,12 @@
+@BOOK{Wurm2011-ho,
+ title = "{Substanz und Qualität : Ein Beitrag zur Interpretation der
+ plotinischen Traktate VI,1, 2 und 3}",
+ author = "Wurm, Klaus",
+ publisher = "De Gruyter",
+ series = "Quellen und Studien zur Philosophie",
+ edition = "Reprint 2011",
+ year = 2011,
+ address = "Berlin",
+ keywords = "!!! Plotinus translation",
+ language = "de"
+} \ No newline at end of file
diff --git a/wasm/examples/csv-table-to-org/options.json b/wasm/examples/csv-table-to-org/options.json
new file mode 100644
index 000000000..9fbc4705b
--- /dev/null
+++ b/wasm/examples/csv-table-to-org/options.json
@@ -0,0 +1,4 @@
+{
+ "from": "csv",
+ "to": "org"
+} \ No newline at end of file
diff --git a/wasm/examples/csv-table-to-org/stdin b/wasm/examples/csv-table-to-org/stdin
new file mode 100644
index 000000000..5915766dc
--- /dev/null
+++ b/wasm/examples/csv-table-to-org/stdin
@@ -0,0 +1,10 @@
+"Year", "Score", "Title"
+1968, 86, "Greetings"
+1970, 17, "Bloody Mama"
+1970, 73, "Hi, Mom!"
+1971, 40, "Born to Win"
+1973, 98, "Mean Streets"
+1973, 88, "Bang the Drum Slowly"
+1974, 97, "The Godfather, Part II"
+1976, 41, "The Last Tycoon"
+1976, 99, "Taxi Driver" \ No newline at end of file
diff --git a/wasm/examples/custom-template/custom.tpl b/wasm/examples/custom-template/custom.tpl
new file mode 100644
index 000000000..d6eeda8a3
--- /dev/null
+++ b/wasm/examples/custom-template/custom.tpl
@@ -0,0 +1,6 @@
+<h1>$title$</h1>
+<p>by $author$</p>
+<p>Keywords: $for(keywords)$$it$$sep$; $endfor$</p>
+<main>
+$body$
+</main>
diff --git a/wasm/examples/custom-template/options.json b/wasm/examples/custom-template/options.json
new file mode 100644
index 000000000..aba47f923
--- /dev/null
+++ b/wasm/examples/custom-template/options.json
@@ -0,0 +1,6 @@
+{
+ "from": "markdown",
+ "to": "html",
+ "template": "custom.tpl",
+ "standalone": true
+} \ No newline at end of file
diff --git a/wasm/examples/custom-template/stdin b/wasm/examples/custom-template/stdin
new file mode 100644
index 000000000..ba028327d
--- /dev/null
+++ b/wasm/examples/custom-template/stdin
@@ -0,0 +1,10 @@
+---
+keywords:
+- bee
+- ant
+- ladybug
+author: E. N. Tymologist
+title: Some bugs
+...
+
+This is a book about bugs.
diff --git a/wasm/examples/docx-with-equations-to-latex/equations.docx b/wasm/examples/docx-with-equations-to-latex/equations.docx
new file mode 100644
index 000000000..26dd2daaa
--- /dev/null
+++ b/wasm/examples/docx-with-equations-to-latex/equations.docx
Binary files differ
diff --git a/wasm/examples/docx-with-equations-to-latex/options.json b/wasm/examples/docx-with-equations-to-latex/options.json
new file mode 100644
index 000000000..1803234e6
--- /dev/null
+++ b/wasm/examples/docx-with-equations-to-latex/options.json
@@ -0,0 +1,6 @@
+{
+ "from": "docx",
+ "to": "latex",
+ "standalone": true,
+ "input-files": ["equations.docx"]
+}
diff --git a/wasm/examples/hello-world/options.json b/wasm/examples/hello-world/options.json
new file mode 100644
index 000000000..4c0ac827e
--- /dev/null
+++ b/wasm/examples/hello-world/options.json
@@ -0,0 +1,4 @@
+{
+ "from": "markdown",
+ "to": "html5"
+} \ No newline at end of file
diff --git a/wasm/examples/hello-world/stdin b/wasm/examples/hello-world/stdin
new file mode 100644
index 000000000..4bedee873
--- /dev/null
+++ b/wasm/examples/hello-world/stdin
@@ -0,0 +1 @@
+*Hello* world! \ No newline at end of file
diff --git a/wasm/examples/highlighted-code-to-html/options.json b/wasm/examples/highlighted-code-to-html/options.json
new file mode 100644
index 000000000..8fa460443
--- /dev/null
+++ b/wasm/examples/highlighted-code-to-html/options.json
@@ -0,0 +1,13 @@
+{
+ "to": "html",
+ "from": "markdown",
+ "standalone": true,
+ "embed-resources": false,
+ "table-of-contents": false,
+ "number-sections": false,
+ "citeproc": false,
+ "html-math-method": "plain",
+ "wrap": "preserve",
+ "highlight-style": "kate",
+ "template": null
+} \ No newline at end of file
diff --git a/wasm/examples/highlighted-code-to-html/stdin b/wasm/examples/highlighted-code-to-html/stdin
new file mode 100644
index 000000000..d5b2a7625
--- /dev/null
+++ b/wasm/examples/highlighted-code-to-html/stdin
@@ -0,0 +1,39 @@
+---
+title: Code with syntax highlighting
+lang: en-US
+...
+
+Here's some code with syntax highlighting:
+
+``` haskell
+-- | Inefficient quicksort in haskell.
+qsort :: (Enum a) => [a] -> [a]
+qsort [] = []
+qsort (x:xs) = qsort (filter (< x) xs) ++ [x] ++
+ qsort (filter (>= x) xs)
+```
+
+Try changing the highlighting style to see what effect this has.
+
+Here's some python, with numbered lines:
+
+``` python {.numberLines}
+class FSM(object):
+
+"""This is a Finite State Machine (FSM).
+"""
+
+def __init__(self, initial_state, memory=None):
+
+ """This creates the FSM. You set the initial state here. The "memory"
+ attribute is any object that you want to pass along to the action
+ functions. It is not used by the FSM. For parsing you would typically
+ pass a list to be used as a stack. """
+
+ # Map (input_symbol, current_state) --> (action, next_state).
+ self.state_transitions = {}
+ # Map (current_state) --> (action, next_state).
+ self.state_transitions_any = {}
+ self.default_transition = None
+ ...
+```
diff --git a/wasm/examples/ipynb-to-rtf/options.json b/wasm/examples/ipynb-to-rtf/options.json
new file mode 100644
index 000000000..88602529b
--- /dev/null
+++ b/wasm/examples/ipynb-to-rtf/options.json
@@ -0,0 +1,5 @@
+{
+ "from": "ipynb",
+ "to": "rtf",
+ "standalone": true
+} \ No newline at end of file
diff --git a/wasm/examples/ipynb-to-rtf/stdin b/wasm/examples/ipynb-to-rtf/stdin
new file mode 100644
index 000000000..5fad9322e
--- /dev/null
+++ b/wasm/examples/ipynb-to-rtf/stdin
@@ -0,0 +1,68 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "# Lorem ipsum\n",
+ "\n",
+ "**Lorem ipsum** dolor sit amet, consectetur adipiscing elit. Nunc luctus\n",
+ "bibendum felis dictum sodales."
+ ],
+ "id": "42a14256-91c8-446e-92a7-ab6bf11055d3"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "print(\"hello\")"
+ ],
+ "id": "98ee7437-e11a-4c16-b642-9e7911f32cd2"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Pyout"
+ ],
+ "id": "2df739f6-1afd-400b-845c-ad1efebc209f"
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from IPython.display import HTML\n",
+ "HTML(\"\"\"\n",
+ "<script>\n",
+ "console.log(\"hello\");\n",
+ "</script>\n",
+ "<b>HTML</b>\n",
+ "\"\"\")"
+ ],
+ "id": "622b77f5-76e7-46cb-a694-56007ed6adbe"
+ },
+ {
+ "cell_type": "markdown",
+ "metadata": {},
+ "source": [
+ "## Image\n",
+ "\n",
+ "This image ![the moon](attachment:lalune.jpg) will be included as a cell\n",
+ "attachment."
+ ],
+ "attachments": {
+ "lalune.jpg": {
+ "image/jpeg": "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\n3FNQTvPJ7UYAelf/2Q==\n"
+ }
+ },
+ "id": "0db42901-5dc0-4fd3-b2db-b83b3c584437"
+ }
+ ],
+ "nbformat": 4,
+ "nbformat_minor": 5,
+ "metadata": {}
+} \ No newline at end of file
diff --git a/wasm/examples/latex-to-docbook-with-mathml/options.json b/wasm/examples/latex-to-docbook-with-mathml/options.json
new file mode 100644
index 000000000..8459c7bf5
--- /dev/null
+++ b/wasm/examples/latex-to-docbook-with-mathml/options.json
@@ -0,0 +1,6 @@
+{
+ "from": "latex",
+ "to": "docbook5",
+ "html-math-method": "mathml",
+ "standalone": true
+} \ No newline at end of file
diff --git a/wasm/examples/latex-to-docbook-with-mathml/stdin b/wasm/examples/latex-to-docbook-with-mathml/stdin
new file mode 100644
index 000000000..a5e3ecf5f
--- /dev/null
+++ b/wasm/examples/latex-to-docbook-with-mathml/stdin
@@ -0,0 +1,27 @@
+\newtheorem{theorem}{Theorem}
+\newtheorem{corollary}[theorem]{Corollary}
+\newtheorem{lemma}[theorem]{Lemma}
+\theoremstyle{definition}
+\newtheorem{definition}[theorem]{Definition}
+\theoremstyle{remark}
+\newtheorem{remark}{Remark}
+
+\begin{definition}[right-angled triangles] \label{def:tri}
+A \emph{right-angled triangle} is a triangle whose sides of length~\(a\), \(b\) and~\(c\), in some permutation of order, satisfies \(a^2+b^2=c^2\).
+\end{definition}
+
+\begin{lemma}
+The triangle with sides of length~\(3\), \(4\) and~\(5\) is right-angled.
+\end{lemma}
+
+\begin{proof}
+This lemma follows from \cref{def:tri} since \(3^2+4^2=9+16=25=5^2\).
+\end{proof}
+
+\begin{theorem}[Pythagorean triplets] \label{thm:py}
+Triangles with sides of length \(a=p^2-q^2\), \(b=2pq\) and \(c=p^2+q^2\) are right-angled triangles.
+\end{theorem}
+
+\begin{remark}
+These are all pretty interesting facts.
+\end{remark}
diff --git a/wasm/examples/latex-with-macros-to-restructured-text/options.json b/wasm/examples/latex-with-macros-to-restructured-text/options.json
new file mode 100644
index 000000000..514250ff3
--- /dev/null
+++ b/wasm/examples/latex-with-macros-to-restructured-text/options.json
@@ -0,0 +1,6 @@
+{
+ "from": "latex",
+ "to": "rst",
+ "standalone": true,
+ "citeproc": false
+} \ No newline at end of file
diff --git a/wasm/examples/latex-with-macros-to-restructured-text/stdin b/wasm/examples/latex-with-macros-to-restructured-text/stdin
new file mode 100644
index 000000000..d7bc93abb
--- /dev/null
+++ b/wasm/examples/latex-with-macros-to-restructured-text/stdin
@@ -0,0 +1,9 @@
+% from https://en.wikibooks.org/wiki/LaTeX/Macros
+\newcommand{\wbalTwo}[2][Wikimedia]{
+This is the Wikibook about LaTeX
+supported by {#1} and {#2}!}
+
+\begin{itemize}
+\item \wbalTwo{John Doe}
+\item \wbalTwo[lots of users]{John Doe}
+\end{itemize} \ No newline at end of file
diff --git a/wasm/examples/man-page-to-context/options.json b/wasm/examples/man-page-to-context/options.json
new file mode 100644
index 000000000..896669a13
--- /dev/null
+++ b/wasm/examples/man-page-to-context/options.json
@@ -0,0 +1,4 @@
+{
+ "from": "man",
+ "to": "context"
+} \ No newline at end of file
diff --git a/wasm/examples/man-page-to-context/stdin b/wasm/examples/man-page-to-context/stdin
new file mode 100644
index 000000000..4f1780776
--- /dev/null
+++ b/wasm/examples/man-page-to-context/stdin
@@ -0,0 +1,40 @@
+.TP
+\f[C]-L\f[R] \f[I]SCRIPT\f[R], \f[C]--lua-filter=\f[R]\f[I]SCRIPT\f[R]
+Transform the document in a similar fashion as JSON filters (see
+\f[C]--filter\f[R]), but use pandoc\[cq]s built-in Lua filtering system.
+The given Lua script is expected to return a list of Lua filters which
+will be applied in order.
+Each Lua filter must contain element-transforming functions indexed by
+the name of the AST element on which the filter function should be
+applied.
+.RS
+.PP
+The \f[C]pandoc\f[R] Lua module provides helper functions for element
+creation.
+It is always loaded into the script\[cq]s Lua environment.
+.PP
+See the Lua filters documentation for further details.
+.PP
+In order of preference, pandoc will look for Lua filters in
+.IP "1." 3
+a specified full or relative path,
+.IP "2." 3
+\f[C]$DATADIR/filters\f[R] where \f[C]$DATADIR\f[R] is the user data
+directory (see \f[C]--data-dir\f[R], above).
+.PP
+Filters, Lua filters, and citeproc processing are applied in the order
+specified on the command line.
+.RE
+.TP
+\f[C]-M\f[R] \f[I]KEY\f[R][\f[C]=\f[R]\f[I]VAL\f[R]], \f[C]--metadata=\f[R]\f[I]KEY\f[R][\f[C]:\f[R]\f[I]VAL\f[R]]
+Set the metadata field \f[I]KEY\f[R] to the value \f[I]VAL\f[R].
+A value specified on the command line overrides a value specified in the
+document using YAML metadata blocks.
+Values will be parsed as YAML boolean or string values.
+If no value is specified, the value will be treated as Boolean true.
+Like \f[C]--variable\f[R], \f[C]--metadata\f[R] causes template
+variables to be set.
+But unlike \f[C]--variable\f[R], \f[C]--metadata\f[R] affects the
+metadata of the underlying document (which is accessible from filters
+and may be printed in some output formats) and metadata values will be
+escaped when inserted into the template. \ No newline at end of file
diff --git a/wasm/examples/markdown-citations-to-plain-with-csl-style/le-tapuscrit-note.csl b/wasm/examples/markdown-citations-to-plain-with-csl-style/le-tapuscrit-note.csl
new file mode 100644
index 000000000..03b69dfc4
--- /dev/null
+++ b/wasm/examples/markdown-citations-to-plain-with-csl-style/le-tapuscrit-note.csl
@@ -0,0 +1,496 @@
+<?xml version="1.0" encoding="utf-8"?>
+<style xmlns="http://purl.org/net/xbiblio/csl" class="note" default-locale="fr-FR" version="1.0" page-range-format="expanded">
+ <info>
+ <title>Le tapuscrit (École des hautes études en sciences sociales) (note, French)</title>
+ <title-short>Tapuscrit-EHESS</title-short>
+ <id>http://www.zotero.org/styles/le-tapuscrit-note</id>
+ <link href="http://www.zotero.org/styles/le-tapuscrit-note" rel="self"/>
+ <link href="http://www.editions.ehess.fr/ouvrages/ouvrage/le-tapuscrit/" rel="documentation"/>
+ <author>
+ <name>Franziska Heimburger</name>
+ <email>[email protected]</email>
+ </author>
+ <category citation-format="note"/>
+ <category field="social_science"/>
+ <category field="generic-base"/>
+ <updated>2018-07-12T11:20:37+00:00</updated>
+ <rights license="http://creativecommons.org/licenses/by-sa/3.0/">This work is licensed under a Creative Commons Attribution-ShareAlike 3.0 License</rights>
+ </info>
+ <locale xml:lang="fr">
+ <terms>
+ <term name="ordinal-01">ère</term>
+ <term name="ordinal-02">e</term>
+ <term name="ordinal-03">e</term>
+ <term name="ordinal-04">e</term>
+ <term name="cited">op.&#160;cit.</term>
+ <term name="page" form="short">p.</term>
+ <term name="editor" form="short">
+ <single>ed.</single>
+ <multiple>eds.</multiple>
+ </term>
+ <term name="in">dans</term>
+ </terms>
+ </locale>
+ <macro name="author">
+ <choose>
+ <if variable="author">
+ <names variable="author">
+ <name form="long" and="text" delimiter-precedes-last="never" sort-separator=" "/>
+ </names>
+ </if>
+ <else-if variable="editor">
+ <names variable="editor">
+ <name form="long" and="text" delimiter-precedes-last="never" sort-separator=" "/>
+ <label form="short" prefix="&#160;(" suffix=".)"/>
+ </names>
+ </else-if>
+ </choose>
+ </macro>
+ <macro name="author-bib">
+ <choose>
+ <if variable="author">
+ <names variable="author">
+ <name name-as-sort-order="all" form="long" and="text" delimiter-precedes-last="never" sort-separator=" ">
+ <name-part name="family" font-variant="small-caps"/>
+ </name>
+ </names>
+ </if>
+ <else-if variable="editor">
+ <names variable="editor">
+ <name name-as-sort-order="all" form="long" and="text" delimiter-precedes-last="never" sort-separator=" ">
+ <name-part name="family" font-variant="small-caps"/>
+ </name>
+ <label form="short" prefix="&#160;(" suffix=".)"/>
+ </names>
+ </else-if>
+ </choose>
+ </macro>
+ <macro name="author-ibid">
+ <choose>
+ <if variable="author">
+ <names variable="author">
+ <name and="text" initialize="true" initialize-with="." delimiter-precedes-last="never" sort-separator=" " font-style="normal"/>
+ </names>
+ </if>
+ <else-if variable="editor">
+ <names variable="editor">
+ <name form="long" and="text" delimiter-precedes-last="never" sort-separator=" "/>
+ <label form="short" prefix="&#160;(" suffix=".)"/>
+ </names>
+ </else-if>
+ </choose>
+ </macro>
+ <macro name="editor">
+ <names variable="editor">
+ <name form="long" and="text" delimiter-precedes-last="never" sort-separator=" "/>
+ <label form="short" prefix="&#160;(" suffix=".)"/>
+ </names>
+ </macro>
+ <macro name="translator">
+ <names variable="translator">
+ <name form="long" and="text" delimiter-precedes-last="never" sort-separator=" " prefix=" traduit par "/>
+ </names>
+ </macro>
+ <macro name="title">
+ <choose>
+ <if type="bill book graphic legal_case motion_picture report song" match="any">
+ <text variable="title" text-case="capitalize-first" font-style="italic"/>
+ </if>
+ <else-if type="article-journal article-newspaper article-magazine" match="any">
+ <group delimiter=", ">
+ <text variable="title" text-case="capitalize-first" quotes="true"/>
+ <text variable="container-title" font-style="italic"/>
+ </group>
+ </else-if>
+ <else-if type="thesis" match="any">
+ <group delimiter="">
+ <text variable="title" text-case="capitalize-first" font-style="italic" suffix=","/>
+ <text variable="genre" suffix=", " prefix=" "/>
+ <text variable="publisher"/>
+ </group>
+ </else-if>
+ <else-if type="manuscript" match="any">
+ <group delimiter=",">
+ <text variable="title" text-case="capitalize-first" font-style="italic"/>
+ <text variable="genre" prefix=" [" suffix="]"/>
+ </group>
+ </else-if>
+ <else-if type="chapter entry-dictionary entry-encyclopedia" match="any">
+ <group delimiter="">
+ <text variable="title" text-case="capitalize-first" quotes="true"/>
+ <text value="dans" suffix=" " prefix=" "/>
+ <text macro="editor" suffix=", "/>
+ <text variable="container-title" text-case="capitalize-first" font-style="italic"/>
+ </group>
+ </else-if>
+ <else-if type="webpage post-weblog" match="any">
+ <group delimiter="">
+ <text variable="title" text-case="capitalize-first" font-style="italic" suffix=", "/>
+ <text variable="URL"/>
+ <group prefix=" , ">
+ <date variable="issued">
+ <date-part name="day" suffix=" "/>
+ <date-part name="month" suffix=" "/>
+ <date-part name="year"/>
+ </date>
+ </group>
+ </group>
+ </else-if>
+ <else>
+ <text variable="title" quotes="true"/>
+ </else>
+ </choose>
+ </macro>
+ <macro name="pub-place">
+ <choose>
+ <if type="bill book chapter entry-dictionary entry-encyclopedia thesis graphic legal_case manuscript motion_picture paper-conference report song" match="any">
+ <choose>
+ <if variable="publisher-place" match="any">
+ <text variable="publisher-place"/>
+ </if>
+ <else>
+ <text value="s.l."/>
+ </else>
+ </choose>
+ </if>
+ </choose>
+ </macro>
+ <macro name="publisher">
+ <choose>
+ <if type="bill book chapter entry-dictionary entry-encyclopedia graphic legal_case motion_picture paper-conference report song" match="any">
+ <text variable="publisher"/>
+ </if>
+ </choose>
+ </macro>
+ <macro name="yearpage">
+ <choose>
+ <if type="bill book graphic legal_case motion_picture paper-conference manuscript report song thesis" match="any">
+ <group delimiter=", ">
+ <date variable="issued">
+ <date-part name="year"/>
+ </date>
+ <group>
+ <text term="volume" form="short" suffix="."/>
+ <text variable="number-of-volumes" prefix=". " suffix="/"/>
+ <text variable="volume"/>
+ </group>
+ <choose>
+ <if variable="locator" match="any">
+ <group delimiter="&#8239;">
+ <label variable="locator" form="short"/>
+ <text variable="locator"/>
+ </group>
+ </if>
+ <else-if variable="locator" match="none">
+ <text variable="number-of-pages" suffix="&#160;p"/>
+ </else-if>
+ </choose>
+ </group>
+ </if>
+ <else-if type="chapter entry-dictionary entry-encyclopedia" match="any">
+ <group delimiter=" ">
+ <date variable="issued">
+ <date-part name="year" suffix=", "/>
+ </date>
+ <group>
+ <text term="volume" form="short" suffix="."/>
+ <text variable="number-of-volumes" prefix=". " suffix="/"/>
+ <text variable="volume" suffix=","/>
+ </group>
+ <choose>
+ <if variable="locator" match="any">
+ <group delimiter="&#8239;">
+ <label variable="locator" form="short"/>
+ <text variable="locator"/>
+ </group>
+ </if>
+ <else-if variable="locator" match="none">
+ <label variable="page" form="short"/>
+ <text variable="page"/>
+ </else-if>
+ </choose>
+ </group>
+ </else-if>
+ <else-if type="article-journal" match="any">
+ <group delimiter=" " font-style="normal">
+ <choose>
+ <if variable="locator" match="any">
+ <group delimiter="&#8239;">
+ <label variable="locator" form="short"/>
+ <text variable="locator"/>
+ </group>
+ </if>
+ <else-if variable="locator" match="none">
+ <label variable="page" form="short"/>
+ <text variable="page"/>
+ </else-if>
+ </choose>
+ </group>
+ </else-if>
+ <else-if type="article-newspaper article-magazine" match="any">
+ <date variable="issued">
+ <date-part name="day" suffix=" "/>
+ <date-part name="month" form="short" suffix=" "/>
+ <date-part name="year"/>
+ </date>
+ <group delimiter=" " font-style="normal">
+ <label variable="page" form="short"/>
+ <text variable="page"/>
+ </group>
+ <group delimiter=" " font-style="normal">
+ <choose>
+ <if variable="locator" match="any">
+ <group delimiter="&#8239;">
+ <label variable="locator" form="short"/>
+ <text variable="locator"/>
+ </group>
+ </if>
+ <else-if variable="locator" match="none">
+ <label variable="page" form="short"/>
+ </else-if>
+ </choose>
+ </group>
+ </else-if>
+ <else-if type="webpage post-weblog" match="any">
+ <group delimiter=" " prefix="(" suffix=")">
+ <text value="consulté le" suffix=" " prefix=" "/>
+ <date variable="accessed" form="text">
+ <date-part name="day"/>
+ <date-part name="month"/>
+ <date-part name="year"/>
+ </date>
+ </group>
+ </else-if>
+ </choose>
+ </macro>
+ <macro name="yearpage-bib">
+ <choose>
+ <if type="bill book graphic legal_case motion_picture paper-conference report song thesis" match="any">
+ <group delimiter=", ">
+ <group delimiter=", ">
+ <date variable="issued">
+ <date-part name="year"/>
+ </date>
+ <group>
+ <text term="volume" form="short" suffix="."/>
+ <text variable="number-of-volumes" prefix=". " suffix="/"/>
+ <text variable="volume"/>
+ </group>
+ <text variable="number-of-pages" suffix="&#160;p"/>
+ </group>
+ <group>
+ <label variable="locator" form="short"/>
+ <text variable="locator"/>
+ </group>
+ </group>
+ </if>
+ <else-if type="chapter entry-dictionary entry-encyclopedia" match="any">
+ <group delimiter=", ">
+ <date variable="issued">
+ <date-part name="year"/>
+ </date>
+ <group>
+ <text term="volume" form="short" suffix="."/>
+ <text variable="number-of-volumes" prefix=". " suffix="/"/>
+ <text variable="volume"/>
+ </group>
+ <group>
+ <label variable="page" form="short"/>
+ <text variable="page" prefix="&#160;"/>
+ </group>
+ </group>
+ </else-if>
+ <else-if type="article-journal chapter" match="any">
+ <group delimiter=" ">
+ <label variable="page" form="short"/>
+ <text variable="page"/>
+ </group>
+ </else-if>
+ <else-if type="article-newspaper article-magazine" match="any">
+ <group delimiter=" ">
+ <date variable="issued">
+ <date-part name="day" suffix=" "/>
+ <date-part name="month" form="short" suffix=" "/>
+ <date-part name="year"/>
+ </date>
+ <label variable="page" form="short"/>
+ <text variable="page"/>
+ </group>
+ </else-if>
+ <else-if type="manuscript">
+ <group delimiter="" font-style="normal">
+ <choose>
+ <if variable="issued">
+ <date variable="issued">
+ <date-part name="day" suffix=" "/>
+ <date-part name="month" suffix=" "/>
+ <date-part name="year"/>
+ </date>
+ </if>
+ <else>
+ <text value="s. d."/>
+ </else>
+ </choose>
+ </group>
+ </else-if>
+ <else-if type="webpage post-weblog" match="any">
+ <group delimiter=" ">
+ <text value="consulté le" suffix=" " prefix=" "/>
+ <date variable="accessed" form="text">
+ <date-part name="day"/>
+ <date-part name="month"/>
+ <date-part name="year"/>
+ </date>
+ </group>
+ </else-if>
+ </choose>
+ </macro>
+ <macro name="edition">
+ <choose>
+ <if type="bill book graphic legal_case motion_picture report song chapter paper-conference" match="any">
+ <choose>
+ <if is-numeric="edition">
+ <group delimiter=" ">
+ <number variable="edition" form="ordinal"/>
+ <text term="edition" form="short"/>
+ </group>
+ </if>
+ <else>
+ <text variable="edition" text-case="capitalize-first" suffix="."/>
+ </else>
+ </choose>
+ </if>
+ <else-if type="article-journal article-magazine" match="any">
+ <group delimiter="">
+ <choose>
+ <if variable="issued">
+ <date variable="issued">
+ <date-part name="day" suffix=" "/>
+ <date-part name="month" suffix=" "/>
+ <date-part name="year"/>
+ </date>
+ <text macro="volume" prefix=", "/>
+ </if>
+ <else>
+ <text macro="volume" text-case="capitalize-first"/>
+ </else>
+ </choose>
+ </group>
+ </else-if>
+ </choose>
+ <text macro="issue" prefix=", "/>
+ </macro>
+ <macro name="volume">
+ <choose>
+ <if is-numeric="volume">
+ <text term="volume" form="short" suffix=".&#160;"/>
+ <text variable="volume"/>
+ </if>
+ <else>
+ <text variable="volume"/>
+ </else>
+ </choose>
+ </macro>
+ <macro name="issue">
+ <choose>
+ <if is-numeric="issue">
+ <text term="issue" form="short" suffix="&#160;"/>
+ <text variable="issue"/>
+ </if>
+ <else>
+ <text variable="issue"/>
+ </else>
+ </choose>
+ </macro>
+ <macro name="collection">
+ <text variable="collection-title" quotes="true" prefix=" (coll.&#160;" suffix=")"/>
+ </macro>
+ <citation et-al-min="4" et-al-use-first="1">
+ <layout suffix="." delimiter="&#160;; ">
+ <choose>
+ <if position="ibid-with-locator">
+ <group delimiter=", ">
+ <text term="ibid" text-case="capitalize-first" font-style="italic" suffix="."/>
+ <text variable="locator" prefix="p.&#160;"/>
+ </group>
+ </if>
+ <else-if position="ibid">
+ <text term="ibid" text-case="capitalize-first" font-style="italic"/>
+ </else-if>
+ <else-if position="subsequent">
+ <group delimiter=", ">
+ <text macro="author-ibid"/>
+ <choose>
+ <if type="bill book graphic legal_case motion_picture report song thesis manuscript" match="any">
+ <text variable="title" form="short" font-style="italic"/>
+ <text term="cited" font-style="italic" suffix="."/>
+ </if>
+ <else>
+ <text variable="title" text-case="capitalize-first" form="short" quotes="true"/>
+ <text value="art cit"/>
+ </else>
+ </choose>
+ <text variable="locator" prefix="p.&#160;"/>
+ </group>
+ </else-if>
+ <else>
+ <choose>
+ <if type="manuscript">
+ <group delimiter=", ">
+ <text variable="archive"/>
+ <text variable="archive_location"/>
+ <text variable="call-number"/>
+ <text macro="title"/>
+ <text macro="yearpage-bib"/>
+ </group>
+ </if>
+ <else-if type="bill chapter article-journal article-newspaper interview book graphic legal_case motion_picture paper-conference report song thesis webpage post-weblog article-magazine" match="any">
+ <group delimiter=", ">
+ <text macro="author"/>
+ <text macro="title"/>
+ <text macro="translator"/>
+ <text macro="edition"/>
+ <text macro="pub-place"/>
+ <text macro="publisher"/>
+ <text macro="yearpage"/>
+ </group>
+ </else-if>
+ </choose>
+ </else>
+ </choose>
+ </layout>
+ </citation>
+ <bibliography>
+ <sort>
+ <key macro="author" names-min="3" names-use-first="3"/>
+ <key variable="issued" sort="descending"/>
+ </sort>
+ <layout suffix=".">
+ <choose>
+ <if type="manuscript">
+ <group delimiter=", ">
+ <text variable="archive"/>
+ <text variable="archive_location"/>
+ <text variable="call-number"/>
+ <text macro="title"/>
+ <text macro="yearpage-bib"/>
+ </group>
+ </if>
+ <else-if type="bill chapter article-journal article-newspaper interview book graphic legal_case motion_picture paper-conference report song thesis webpage post-weblog article-magazine" match="any">
+ <group delimiter=", ">
+ <text macro="author-bib"/>
+ <text macro="title"/>
+ <text macro="translator"/>
+ <text macro="edition"/>
+ <text macro="pub-place"/>
+ <group delimiter=" ">
+ <text macro="publisher"/>
+ <text macro="collection"/>
+ </group>
+ <text macro="yearpage-bib"/>
+ </group>
+ </else-if>
+ </choose>
+ </layout>
+ </bibliography>
+</style> \ No newline at end of file
diff --git a/wasm/examples/markdown-citations-to-plain-with-csl-style/options.json b/wasm/examples/markdown-citations-to-plain-with-csl-style/options.json
new file mode 100644
index 000000000..78ec5985a
--- /dev/null
+++ b/wasm/examples/markdown-citations-to-plain-with-csl-style/options.json
@@ -0,0 +1,7 @@
+{
+ "from": "markdown",
+ "to": "plain",
+ "citeproc": true,
+ "csl": "le-tapuscrit-note.csl",
+ "bibliography": "refs.bib"
+}
diff --git a/wasm/examples/markdown-citations-to-plain-with-csl-style/refs.bib b/wasm/examples/markdown-citations-to-plain-with-csl-style/refs.bib
new file mode 100644
index 000000000..ec7a9e761
--- /dev/null
+++ b/wasm/examples/markdown-citations-to-plain-with-csl-style/refs.bib
@@ -0,0 +1,9 @@
+@book{legras_michel_2010,
+ author = {Le~Gras, Gwénaëlle},
+ publisher = {Scope},
+ title = {Michel Simon~: l’art de la disgrâce},
+ series = {Jeux d’acteurs},
+ date = {2010},
+ address = {Paris},
+ isbn = {978-2-912573-52-0},
+ langid = {fre}} \ No newline at end of file
diff --git a/wasm/examples/markdown-citations-to-plain-with-csl-style/stdin b/wasm/examples/markdown-citations-to-plain-with-csl-style/stdin
new file mode 100644
index 000000000..1b1c0f5d0
--- /dev/null
+++ b/wasm/examples/markdown-citations-to-plain-with-csl-style/stdin
@@ -0,0 +1,7 @@
+---
+csl: 'le-tapuscrit-note.csl'
+lang: fr-FR
+bibliography: refs.bib
+---
+
+Foo [@legras_michel_2010]. \ No newline at end of file
diff --git a/wasm/examples/markdown-to-docbook-with-citations/options.json b/wasm/examples/markdown-to-docbook-with-citations/options.json
new file mode 100644
index 000000000..0d3cddc97
--- /dev/null
+++ b/wasm/examples/markdown-to-docbook-with-citations/options.json
@@ -0,0 +1,6 @@
+{
+ "from": "markdown",
+ "to": "docbook5",
+ "standalone": true,
+ "citeproc": true
+} \ No newline at end of file
diff --git a/wasm/examples/markdown-to-docbook-with-citations/stdin b/wasm/examples/markdown-to-docbook-with-citations/stdin
new file mode 100644
index 000000000..9a965f117
--- /dev/null
+++ b/wasm/examples/markdown-to-docbook-with-citations/stdin
@@ -0,0 +1,22 @@
+---
+references:
+- author:
+ - family: Salam
+ given: Abdus
+ container-title: "Elementary particle theory: Relativistic groups and
+ analyticity. Proceedings of the eighth Nobel symposium"
+ editor:
+ - family: Svartholm
+ given: Nils
+ event-date: 1968-05-19/1968-05-25
+ event-place: Aspenäsgarden, Lerum
+ id: salam
+ issued: 1968
+ page: 367-377
+ publisher: Almquist & Wiksell
+ publisher-place: Stockholm
+ title: Weak and electromagnetic interactions
+ type: paper-conference
+---
+
+@salam [p. 370] says some interesting things. \ No newline at end of file
diff --git a/wasm/examples/markdown-to-revealjs-slides/options.json b/wasm/examples/markdown-to-revealjs-slides/options.json
new file mode 100644
index 000000000..ab945447a
--- /dev/null
+++ b/wasm/examples/markdown-to-revealjs-slides/options.json
@@ -0,0 +1,9 @@
+{
+ "to": "revealjs",
+ "from": "markdown",
+ "standalone": true,
+ "citeproc": false,
+ "html-math-method": "mathjax",
+ "highlight-style": "pygments",
+ "template": null
+} \ No newline at end of file
diff --git a/wasm/examples/markdown-to-revealjs-slides/stdin b/wasm/examples/markdown-to-revealjs-slides/stdin
new file mode 100644
index 000000000..2330117cc
--- /dev/null
+++ b/wasm/examples/markdown-to-revealjs-slides/stdin
@@ -0,0 +1,560 @@
+---
+title: Pandoc for TeXnicians
+author: John MacFarlane
+date: TUG 2020, 2020-07-26
+theme: solarized
+header-includes: |
+ <style>
+ .reveal {
+ font-size: 20pt;
+ line-height: 1.2em;
+ }
+ .reveal pre code {
+ font-size: 16pt;
+ line-height: 1.2em;
+ }
+ </style>
+...
+
+# Overview
+
+##
+
+- What is pandoc?
+- Using pandoc to convert to and from LaTeX
+- Why write in Markdown?
+- Overcoming Markdown's limitations
+
+# What is pandoc?
+
+##
+
+<https://pandoc.org>
+
+## Let's take it for a spin
+
+```
+% cat simple.tex
+\section{On $e=mc^2$}\label{einstein}
+```
+
+```
+% pandoc -f latex -t native simple.tex
+% pandoc -f latex -t html simple.tex
+% pandoc -t html --mathml simple.tex
+% pandoc -t html --mathjax simple.tex
+% pandoc -t -html --mathjax -s simple.tex
+% pandoc -t ms simple.tex
+% pandoc -t gfm simple.tex
+% pandoc -t context simple.tex
+% pandoc -t jats simple.tex
+```
+
+
+## Some math
+
+Let's try with a sample TeX document by Professor A.J. Roberts
+at the University of Adelaide (CC licensed).
+
+<http://www.maths.adelaide.edu.au/anthony.roberts/LaTeX/Src/maths.tex>
+
+## Some math
+
+```
+% pandoc maths.tex -o maths.docx
+```
+
+. . .
+
+Two problems:
+
+- the use of a low-level TeX primitive `\mathcode`.
+- the use of `\parbox` (line 288)
+
+Fix by removing the `\mathcode` stuff and
+redefining the `\parmath` macro as a no-op:
+
+```latex
+\newcommand{\parmath}[2][]{#2}
+```
+
+## Take two
+
+```
+% pandoc maths.tex --number-sections -o maths.docx
+% open maths.docx
+```
+
+- AMS theorem environments come out right, including references.
+- Math is translated into native Word equation objects, which
+ can be edited and which match the font, rather than images.
+- Still missing: equation numbers.
+
+## Going the other way
+
+```
+% pandoc maths.docx -o newmaths.tex -s
+% xelatex newmaths
+% xelatex newmaths
+```
+
+## Converting to HTML
+
+```
+% pandoc maths.tex -s -o maths.html --mathml \
+ --number-sections --toc
+% open maths.html
+```
+
+## Comparison with latex2rtf
+
+```
+% latex2rtf maths.tex
+% open -a "Microsoft Word" maths.rtf
+```
+
+- References not resolved in Section 1
+- Accents in Section 2 not above the letters, math generally
+ ugly
+- Arrays in Section 8 totally broken; same with subequations in
+ Section 9
+- But at least we do get equation numbers in Section 9
+
+## Comparison with tex4ht
+
+```
+% make4ht maths
+% open maths.html
+```
+
+- Theorem environments not handled in Section 1 (except for one?).
+- Missing accents in Section 2.
+- Ugly equations that incorporate both text and images in
+ different fonts.
+
+## Comparison with Word from PDF
+
+```
+% pdflatex maths
+% pdflatex maths
+% open -a "Microsoft Word" maths.pdf
+```
+
+- Section 2, accents messed up.
+- Some formulas are rendered with images, others with
+ regular characters, in non-matching font.
+- The 'where' in Section 6 is badly mispleacd.
+- The integral is missing in Section 7
+- The diagonal ellipses are missing in the arrays
+
+
+## Pandoc can interpret TeX macros
+
+```
+% cat macros.tex
+\newcommand{\nec}{\Box}
+\newcommand{\if}[2]{#1 \rightarrow #2}
+\newenvironment{warning}%
+ {\begin{quote}\textbf{WARNING!}}%
+ {\end{quote}}
+
+$\if{\nec \phi}{\phi}$
+\begin{warning}
+Don't try this at home.
+\end{warning}
+```
+
+```
+% pandoc macros.tex -t html
+```
+
+## Pandoc can resolve bibtex citations
+
+With the help of the `pandoc-citeproc` filter
+(included in the released binaries).
+
+```
+% pandoc --filter pandoc-citeproc bib.tex \
+ -t plain --csl ieee.csl
+```
+
+## Limitations
+
+Pandoc is far from being able
+to convert arbitrary tex files with high accuracy.
+
+Let's try with a real-world example I got at random from arxiv.
+
+```
+% cd arxiv.2007.07694v1
+% pandoc arxiv.tex -o arxiv.docx
+```
+
+# An alternative
+
+## An alternative
+
+So you can't just write in LaTeX and expect to convert at the
+last minute to docx (for a publisher) or epub (for your
+students) or HTML (for your website).
+
+An alternative: write your document
+in pandoc's extended version of Markdown, which pandoc
+can convert with complete accuracy to any of its
+output formats.
+
+
+## What is Markdown?
+
+Markdown is a set of conventions for indicating document
+formatting in plain text, mostly inherited from the pre-internet
+days of bulletin boards and email.
+
+It was designed in 2004 by John Gruber with help from Aaron
+Schwartz, and it is currently much used by programmers,
+and on forums like stackoverflow and reddit, and by
+data scientists via Jupyter notebooks and RMarkdown.
+
+<https://daringfireball.net/projects/markdown/>
+
+## Appealing things about Markdown
+
+The source text is readable as it is.
+When writing and revising, you don't have
+to parse through command-words which aren't part
+of the content.
+
+. . .
+
+If you're writing in a language other than English, you
+don't have to have English words sprinkled in the text.
+
+. . .
+
+There's no boilerplate at the beginning. The document
+just starts with the text.
+
+## Real separation of content from formatting.
+
+\vspace{1em}
+
+> The paucity of means is the greatest virtue of markdown and
+> pandoc markdown.
+>
+> It is strangely difficult to get people to see the point, but the
+> defects of LaTeX for concentration, writing and thought, are at least
+> as great as those of Word, for the simple reason that it gives the
+> writer too much power; there is always another package to call in the
+> preamble, as there is always another drop down menu in Word.
+> ...
+>
+> In markdown - not to put too fine a point on it - the writer is only
+> ever faced with one question, and it is the right one: what the next
+> sentence should be.
+>
+> --- Michael Thompson, pandoc-discuss mailing list
+
+
+## Appealing things about Markdown
+
+Using Markdown makes it possible to collaborate with
+others who don't know LaTeX.
+
+## Appealing things about Markdown
+
+Markdown can be converted with complete, reliable accuracy
+into many different formats.
+
+It's often not enough just to produce a PDF.
+
+- JATS for publication or archiving
+- EPUB for convenient reading on mobile devices
+- Docx or ICML for a publisher
+- HTML for a website (or accessibility)
+- Jupyter notebook for research
+- Beamer or reveal.js slides for presentation
+
+TeX is a great assembly language for publication-quality
+documents.
+
+## Limitations of Markdown
+
+John Gruber's original markdown syntax lacks support for:
+
+- [ ] tables
+- [ ] figures
+- [ ] footnotes
+- [ ] definition lists
+- [ ] ordered lists other than decimal-numbered
+- [ ] super/subscript
+- [ ] math
+- [ ] document metadata
+- [ ] attributes or metadata on individual elements like sections
+- [ ] labels and cross-references
+- [ ] numbering for running examples or equations
+
+## Limitations of Markdown
+
+We couldn't live without these things in academic writing.
+
+And we definitely couldn't live without
+
+- [ ] bibtex/biblatex
+- [ ] macros
+
+How can we overcome these limitations?
+
+# Overcoming Markdown's limitations
+
+## Pandoc's extended Markdown syntax
+
+- [x] tables (limited)
+- [x] figures (limited)
+- [x] math
+- [x] footnotes
+- [x] definition lists
+- [x] more flexible ordered lists
+- [x] running example lists
+- [x] super/subscript
+- [x] strikeout
+- [x] metadata
+- [x] attributes
+- [x] generic containers
+
+##
+
+Pandoc also understands LaTeX macro definitions, which
+you can use for math (no matter what the output format).
+
+##
+
+Labels and cross-references are still a work in progress,
+but you can get good support for them using an external
+filter, `pandoc-crossref`, by pandoc contributor
+Nikolay Yakimov.
+
+##
+
+You can use the `--citeproc` filter to resolve citations
+in this syntax:
+
+```
+Blah blah [@putnam:empirical, p. 33; see also
+@dummett:empirical].
+```
+
+Change the style by specifying a CSL stylesheet.
+(You can even change between author-date, numerical,
+and footnote sytles with no modifications to the source.)
+
+You can use your existing bibtex or biblatex bibliography
+file, or a CSL JSON bibliography such as can be produced
+by Zotero.
+
+##
+
+LaTeX macros allow you to define new constructions
+that exactly fit what you're writing about. Can
+we recover this flexibility?
+
+## Raw TeX in Markdown
+
+One approach is to just include bits of raw TeX in
+your markdown file. Pandoc allows that.
+
+- There is a special syntax for indicating chunks of raw TeX,
+ but pandoc will also recognize obvious bits of raw TeX
+ and pass them through as such.
+
+- The raw TeX chunks will be passed on unchanged if the output format
+ is `latex`, `beamer`, or `context`, and otherwise simply omitted.
+
+##
+
+```
+% cat raw.md
+% pandoc raw.md -o raw.pdf
+% open raw.pdf
+```
+But:
+```
+% pandoc raw.md -s -o raw.html
+% open raw.html
+```
+
+
+##
+
+Drawbacks:
+
+- With this approach you lose the ability to
+ target multiple formats.
+- Your source is now an ugly mix of Markdown and
+ TeX, compromising readability.
+
+
+## A better approach
+
+1. Adopt the convention that
+ a certain thing representable in pandoc's markdown
+ should be interpreted as, say, a dropped capital letter.
+
+2. Write a filter that does the interpretation.
+
+## Example: drop caps
+
+In LaTeX we can use the `lettrine` package to
+get dropped capitals at the beginning of chapters:
+
+```latex
+\lettrine{T}{his} is a pulley
+```
+
+We will use a generic bracketed span with a class
+to represent this in Markdown:
+
+```
+[This]{.dropcap} is a pulley.
+
+```
+
+## Example: drop caps
+
+Now we need a filter that replaces
+`Span` elements with class `dropcap` in the Pandoc AST
+with something appropriate for the output format.
+
+![](pandoc-architecture.pdf){height=3in}
+
+## Two kinds of filters
+
+- **JSON filters** operate on a serialized JSON
+representation of the pandoc AST. They
+can be written in any language that can consume
+and produce JSON.
+
+- **Lua filters** use a Lua interpreter
+and environment built into pandoc.
+No external software need be installed, and
+the filters are more efficient,
+because we don't need to serialize and deserialize
+as JSON.
+
+Documentation: https://pandoc.org/lua-filters.html
+
+## Example: drop caps
+
+In a Lua filter we define functions that match
+different kinds of AST elements. Here we want to
+match a Span. Create a file `dropcap.lua`:
+
+```lua
+function Span(el)
+ -- do something with the Span (el)
+ -- return the transformed element or a new element
+end
+```
+
+## Example: drop caps
+
+We only want to do something if the Span has the
+class `dropcap` and its contents begin with a Str
+element.
+
+```lua
+function Span(el)
+ if el.classes:includes('dropcap') then
+ return make_dropcap(el.content)
+ end
+end
+```
+
+## Example: drop caps
+
+Now we just have to define `make_dropcap`. It takes
+a list of Inline elements (`el.content`) and returns
+a list of Inline elements.
+
+\small
+
+```lua
+local function make_dropcap(els)
+ if els[1] and els[1].t == 'Str' then -- arrays start at 1!
+ local first_letter, rest = els[1].text:match('(%a)(.*)')
+ if FORMAT == 'latex' then
+ els[1] = pandoc.RawInline('latex',
+ '\\lettrine{' .. first_letter ..
+ '}{' .. rest .. '}')
+ elseif FORMAT:match('html') then
+ els[1] = pandoc.Span({
+ pandoc.Span(pandoc.Str(first_letter),
+ {class='dropcap-first'}),
+ pandoc.Span(pandoc.Str(rest),
+ {class='dropcap-rest'})})
+ end
+ return els
+ end
+end
+```
+
+## Example: drop caps
+
+```
+% pandoc -L dropcap.lua -t latex -o dropcap.pdf
+% pandoc -L dropcap.lua -t html -s --css dropcap.css \
+ dropcap.md -o dropcap.html
+```
+
+## Example: tikz diagrams
+
+To get a tikz diagram, we could have a filter turn
+specially marked code blocks into images.
+
+In fact, there is already a very nice general
+diagram filter at https://github.com/pandoc/lua-filters.
+
+```
+% cat diagram.md
+% pandoc diagram.md -L diagram-generator.lua -s \
+ --extract-media=media -o diagram.html
+% pandoc diagram.md -L diagram-generator.lua \
+ -o diagram.docx
+```
+
+## Example: theorems
+
+How to reproduce LaTeX `theorem` environments?
+
+Markdown version:
+```
+::: {.theorem #pythagoras}
+#### Pythagoras's Theorem
+In a right triangle, the lengths of the two shorter sides
+$a$, $b$ and the longer side $c$ stand in the relation
+$$
+a^2 + b^2 = c^2.
+$$
+:::
+```
+
+## Example: theorems
+
+```
+% cat theorem.lua
+% cat theorem.md
+% pandoc -L theorem.lua theorem.md -t latex
+% pandoc theorem.md -L theorem.lua -t plain
+% pandoc theorem.md -L theorem.lua -t rst
+% pandoc theorem.md -L theorem.lua -t html
+```
+
+## The end
+
+- For pandoc questions, come to pandoc-discuss on google groups:
+ <https://groups.google.com/g/pandoc-discuss>
+- For bug reports, the tracker at https://github.com/jgm/pandoc
+- If you'd like to improve pandoc's handling of LaTeX,
+ we can always use new contributors!
+
+Questions?
+
diff --git a/wasm/examples/markdown-to-rst/options.json b/wasm/examples/markdown-to-rst/options.json
new file mode 100644
index 000000000..750f8df4e
--- /dev/null
+++ b/wasm/examples/markdown-to-rst/options.json
@@ -0,0 +1,10 @@
+{
+ "to": "rst",
+ "from": "markdown",
+ "standalone": true,
+ "embed-resources": false,
+ "citeproc": false,
+ "html-math-method": "plain",
+ "wrap": "auto",
+ "template": null
+} \ No newline at end of file
diff --git a/wasm/examples/markdown-to-rst/stdin b/wasm/examples/markdown-to-rst/stdin
new file mode 100644
index 000000000..7c7cd7c8a
--- /dev/null
+++ b/wasm/examples/markdown-to-rst/stdin
@@ -0,0 +1,250 @@
+---
+author:
+- Albert Krewinkel
+- John MacFarlane
+date: 'January 10, 2020'
+title: Pandoc Lua Filters
+---
+
+# Introduction
+
+Pandoc has long supported filters, which allow the pandoc
+abstract syntax tree (AST) to be manipulated between the parsing
+and the writing phase. [Traditional pandoc
+filters](https://pandoc.org/filters.html) accept a JSON
+representation of the pandoc AST and produce an altered JSON
+representation of the AST. They may be written in any
+programming language, and invoked from pandoc using the
+`--filter` option.
+
+Although traditional filters are very flexible, they have a
+couple of disadvantages. First, there is some overhead in
+writing JSON to stdout and reading it from stdin (twice, once on
+each side of the filter). Second, whether a filter will work
+will depend on details of the user's environment. A filter may
+require an interpreter for a certain programming language to be
+available, as well as a library for manipulating the pandoc AST
+in JSON form. One cannot simply provide a filter that can be
+used by anyone who has a certain version of the pandoc
+executable.
+
+Starting with version 2.0, pandoc makes it possible to write
+filters in Lua without any external dependencies at all. A Lua
+interpreter (version 5.3) and a Lua library for creating pandoc
+filters is built into the pandoc executable. Pandoc data types
+are marshaled to Lua directly, avoiding the overhead of writing
+JSON to stdout and reading it from stdin.
+
+Here is an example of a Lua filter that converts strong emphasis
+to small caps:
+
+``` lua
+return {
+ {
+ Strong = function (elem)
+ return pandoc.SmallCaps(elem.c)
+ end,
+ }
+}
+```
+
+or equivalently,
+
+``` lua
+function Strong(elem)
+ return pandoc.SmallCaps(elem.c)
+end
+```
+
+This says: walk the AST, and when you find a Strong element,
+replace it with a SmallCaps element with the same content.
+
+To run it, save it in a file, say `smallcaps.lua`, and invoke
+pandoc with `--lua-filter=smallcaps.lua`.
+
+Here's a quick performance comparison, converting the pandoc
+manual (MANUAL.txt) to HTML, with versions of the same JSON
+filter written in compiled Haskell (`smallcaps`) and interpreted
+Python (`smallcaps.py`):
+
+ Command Time
+ --------------------------------------- -------
+ `pandoc` 1.01s
+ `pandoc --filter ./smallcaps` 1.36s
+ `pandoc --filter ./smallcaps.py` 1.40s
+ `pandoc --lua-filter ./smallcaps.lua` 1.03s
+
+As you can see, the Lua filter avoids the substantial overhead
+associated with marshaling to and from JSON over a pipe.
+
+# Lua filter structure
+
+Lua filters are tables with element names as keys and values
+consisting of functions acting on those elements.
+
+Filters are expected to be put into separate files and are
+passed via the `--lua-filter` command-line argument. For
+example, if a filter is defined in a file `current-date.lua`,
+then it would be applied like this:
+
+ pandoc --lua-filter=current-date.lua -f markdown MANUAL.txt
+
+The `--lua-filter` option may be supplied multiple times. Pandoc
+applies all filters (including JSON filters specified via
+`--filter` and Lua filters specified via `--lua-filter`) in the
+order they appear on the command line.
+
+Pandoc expects each Lua file to return a list of filters. The
+filters in that list are called sequentially, each on the result
+of the previous filter. If there is no value returned by the
+filter script, then pandoc will try to generate a single filter
+by collecting all top-level functions whose names correspond to
+those of pandoc elements (e.g., `Str`, `Para`, `Meta`, or
+`Pandoc`). (That is why the two examples above are equivalent.)
+
+For each filter, the document is traversed and each element
+subjected to the filter. Elements for which the filter contains
+an entry (i.e. a function of the same name) are passed to Lua
+element filtering function. In other words, filter entries will
+be called for each corresponding element in the document,
+getting the respective element as input.
+
+The return value of a filter function must be one of the
+following:
+
+- nil: this means that the object should remain unchanged.
+- a pandoc object: this must be of the same type as the input
+ and will replace the original object.
+- a list of pandoc objects: these will replace the original
+ object; the list is merged with the neighbors of the
+ original objects (spliced into the list the original object
+ belongs to); returning an empty list deletes the object.
+
+The function's output must result in an element of the same type
+as the input. This means a filter function acting on an inline
+element must return either nil, an inline, or a list of inlines,
+and a function filtering a block element must return one of nil,
+a block, or a list of block elements. Pandoc will throw an error
+if this condition is violated.
+
+If there is no function matching the element's node type, then
+the filtering system will look for a more general fallback
+function. Two fallback functions are supported, `Inline` and
+`Block`. Each matches elements of the respective type.
+
+Elements without matching functions are left untouched.
+
+See [module documentation](#module-pandoc) for a list of pandoc
+elements.
+
+## Filters on element sequences
+
+For some filtering tasks, it is necessary to know the order
+in which elements occur in the document. It is not enough then to
+inspect a single element at a time.
+
+There are two special function names, which can be used to define
+filters on lists of blocks or lists of inlines.
+
+[`Inlines (inlines)`]{#inlines-filter}
+: If present in a filter, this function will be called on all
+ lists of inline elements, like the content of a [Para]
+ (paragraph) block, or the description of an [Image]. The
+ `inlines` argument passed to the function will be a [List] of
+ [Inline] elements for each call.
+
+[`Blocks (blocks)`]{#blocks-filter}
+: If present in a filter, this function will be called on all
+ lists of block elements, like the content of a [MetaBlocks]
+ meta element block, on each item of a list, and the main
+ content of the [Pandoc] document. The `blocks` argument
+ passed to the function will be a [List] of [Block] elements
+ for each call.
+
+These filter functions are special in that the result must either
+be nil, in which case the list is left unchanged, or must be a
+list of the correct type, i.e., the same type as the input
+argument. Single elements are **not** allowed as return values,
+as a single element in this context usually hints at a bug.
+
+See ["Remove spaces before normal citations"][Inlines filter
+example] for an example.
+
+This functionality has been added in pandoc 2.9.2.
+
+[Inlines filter example]: #remove-spaces-before-citations
+
+## Traversal order
+
+The traversal order of filters can be selected by setting the key
+`traverse` to either `'topdown'` or `'typewise'`; the default is
+`'typewise'`.
+
+Example:
+
+``` lua
+local filter = {
+ traverse = 'topdown',
+ -- ... filter functions ...
+}
+return {filter}
+```
+
+Support for this was added in pandoc 2.17; previous versions
+ignore the `traverse` setting.
+
+### Typewise traversal
+
+Element filter functions within a filter set are called in a
+fixed order, skipping any which are not present:
+
+ 1. functions for [*Inline* elements](#type-inline),
+ 2. the [`Inlines`](#inlines-filter) filter function,
+ 2. functions for [*Block* elements](#type-block) ,
+ 2. the [`Blocks`](#inlines-filter) filter function,
+ 3. the [`Meta`](#type-meta) filter function, and last
+ 4. the [`Pandoc`](#type-pandoc) filter function.
+
+It is still possible to force a different order by explicitly
+returning multiple filter sets. For example, if the filter for
+*Meta* is to be run before that for *Str*, one can write
+
+``` lua
+-- ... filter definitions ...
+
+return {
+ { Meta = Meta }, -- (1)
+ { Str = Str } -- (2)
+}
+```
+
+Filter sets are applied in the order in which they are returned.
+All functions in set (1) are thus run before those in (2),
+causing the filter function for *Meta* to be run before the
+filtering of *Str* elements is started.
+
+### Topdown traversal
+
+It is sometimes more natural to traverse the document tree
+depth-first from the root towards the leaves, and all in a single
+run.
+
+For example, a block list `[Plain [Str "a"], Para [Str
+"b"]]`{.haskell} will try the following filter functions, in
+order: `Blocks`, `Plain`, `Inlines`, `Str`, `Para`, `Inlines`,
+`Str`.
+
+Topdown traversals can be cut short by returning `false` as a
+second value from the filter function. No child-element of
+the returned element is processed in that case.
+
+For example, to exclude the contents of a footnote from being
+processed, one might write
+
+``` lua
+traverse = 'topdown'
+function Note (n)
+ return n, false
+end
+```
+
diff --git a/wasm/examples/mediawiki-to-docx-with-equations/options.json b/wasm/examples/mediawiki-to-docx-with-equations/options.json
new file mode 100644
index 000000000..a8f168ca1
--- /dev/null
+++ b/wasm/examples/mediawiki-to-docx-with-equations/options.json
@@ -0,0 +1,6 @@
+{
+ "from": "mediawiki",
+ "to": "docx",
+ "output-file": "vectors.docx",
+ "standalone": true
+}
diff --git a/wasm/examples/mediawiki-to-docx-with-equations/stdin b/wasm/examples/mediawiki-to-docx-with-equations/stdin
new file mode 100644
index 000000000..18cb37bbb
--- /dev/null
+++ b/wasm/examples/mediawiki-to-docx-with-equations/stdin
@@ -0,0 +1,8 @@
+Just as the components of a vector change when we change the [[basis (linear algebra)|basis]] of the vector space, the components of a tensor also change under such a transformation. Each type of tensor comes equipped with a ''transformation law'' that details how the components of the tensor respond to a [[change of basis]]. The components of a vector can respond in two distinct ways to a [[change of basis]] (see [[covariance and contravariance of vectors]]), where the new [[basis vectors]] <math>\mathbf{\hat{e}}_i</math> are expressed in terms of the old basis vectors <math>\mathbf{e}_j</math> as,
+:<math>\mathbf{\hat{e}}_i = \sum_{j=1}^n \mathbf{e}_j R^j_i = \mathbf{e}_j R^j_i .</math>
+
+Here ''R''<sup>'' j''</sup><sub>''i''</sub> are the entries of the change of basis matrix, and in the rightmost expression the [[summation]] sign was suppressed: this is the [[Einstein summation convention]], which will be used throughout this article.<ref group="Note">The Einstein summation convention, in brief, requires the sum to be taken over all values of the index whenever the same symbol appears as a subscript and superscript in the same term. For example, under this convention <math>B_i C^i = B_1 C^1 + B_2 C^2 + \cdots B_n C^n</math></ref> The components ''v''<sup>''i''</sup> of a column vector '''v''' transform with the [[matrix inverse|inverse]] of the matrix ''R'',
+:<math>\hat{v}^i = \left(R^{-1}\right)^i_j v^j,</math>
+
+where the hat denotes the components in the new basis. This is called a ''contravariant'' transformation law, because the vector components transform by the ''inverse'' of the change of basis. In contrast, the components, ''w''<sub>''i''</sub>, of a covector (or row vector), '''w''', transform with the matrix ''R'' itself,
+:<math>\hat{w}_i = w_j R^j_i .</math> \ No newline at end of file
diff --git a/wasm/examples/ris-to-formatted-markdown-bibliography/options.json b/wasm/examples/ris-to-formatted-markdown-bibliography/options.json
new file mode 100644
index 000000000..ff17f00b8
--- /dev/null
+++ b/wasm/examples/ris-to-formatted-markdown-bibliography/options.json
@@ -0,0 +1,9 @@
+{
+ "to": "markdown_strict",
+ "from": "ris",
+ "standalone": false,
+ "embed-resources": false,
+ "citeproc": true,
+ "html-math-method": "mathml",
+ "wrap": "auto"
+}
diff --git a/wasm/examples/ris-to-formatted-markdown-bibliography/stdin b/wasm/examples/ris-to-formatted-markdown-bibliography/stdin
new file mode 100644
index 000000000..f0ba798ff
--- /dev/null
+++ b/wasm/examples/ris-to-formatted-markdown-bibliography/stdin
@@ -0,0 +1,20 @@
+TY - JOUR
+T1 - On computable numbers, with an application to the Entscheidungsproblem
+A1 - Turing, Alan Mathison
+JO - Proceedings of London Mathematical Society
+VL - 47
+IS - 1
+SP - 230
+EP - 265
+Y1 - 1937
+ER -
+TY - JOUR
+AU - Shannon, Claude E.
+PY - 1948
+DA - July
+TI - A Mathematical Theory of Communication
+T2 - Bell System Technical Journal
+SP - 379
+EP - 423
+VL - 27
+ER -
diff --git a/wasm/examples/rst-table-from-yaml-data/custom.rst b/wasm/examples/rst-table-from-yaml-data/custom.rst
new file mode 100644
index 000000000..cc7eb0d5d
--- /dev/null
+++ b/wasm/examples/rst-table-from-yaml-data/custom.rst
@@ -0,0 +1,7 @@
++------------------+----------+-----------------------------------------------+
+| Species | Calories | Location |
++==================+==========+===============================================+
+$for(fish)$
+${ it:species()/left 16 "| " " | "}${ it.calories/right 8 "" " | " }${ it.location/left 45 "" " |"}
++------------------+----------+-----------------------------------------------+
+$endfor$ \ No newline at end of file
diff --git a/wasm/examples/rst-table-from-yaml-data/options.json b/wasm/examples/rst-table-from-yaml-data/options.json
new file mode 100644
index 000000000..1ec0364f3
--- /dev/null
+++ b/wasm/examples/rst-table-from-yaml-data/options.json
@@ -0,0 +1,8 @@
+{
+ "to": "rst",
+ "from": "markdown",
+ "standalone": true,
+ "citeproc": false,
+ "html-math-method": "plain",
+ "template": "custom.rst"
+}
diff --git a/wasm/examples/rst-table-from-yaml-data/species.rst b/wasm/examples/rst-table-from-yaml-data/species.rst
new file mode 100644
index 000000000..4558c2d7c
--- /dev/null
+++ b/wasm/examples/rst-table-from-yaml-data/species.rst
@@ -0,0 +1,2 @@
+${ it.species_name/uppercase }
+*${ it.scientific_name }*
diff --git a/wasm/examples/rst-table-from-yaml-data/stdin b/wasm/examples/rst-table-from-yaml-data/stdin
new file mode 100644
index 000000000..5a0fe6a9c
--- /dev/null
+++ b/wasm/examples/rst-table-from-yaml-data/stdin
@@ -0,0 +1,721 @@
+---
+# Data from https://www.fishwatch.gov
+fish:
+- calories: 92
+ human_health:
+ texture: Firm and meaty.
+ environmental_considerations:
+ species_name: Shortfin Squid
+ diseases_in_salmon:
+ path: |
+ /profiles/shortfin-squid
+ habitat_impacts: |
+ Fishing gears used to harvest shortfin squid have minimal impacts on
+ habitat.
+ location: |
+ - Shortfin squid inhabits the continental shelf and slope waters of
+ the Northwest Atlantic Ocean, from Newfoundland to the central east
+ coast of Florida.
+ - In the northwest Atlantic Ocean, shortfin squid are most often
+ caught along the continental shelf break in depths between 150 to
+ 275 meters.
+ color: |
+ Raw squid is ivory colored with orange speckling and a brown stripe that
+ runs down the mantle. Cooked squid is opaque white.
+ species_aliases: |
+ [Illex squid](/species-aliases/illex-squid), [Summer
+ squid](/species-aliases/summer-squid)
+ image_gallery:
+ harvest_type: Wild
+ selenium: 44.8 mcg
+ management:
+ scientific_name: Illex illecebrosus
+ production:
+ fat_total: 1.38 g
+ bycatch: |
+ Regulations are in place to minimize bycatch.
+ availability: |
+ Summer and fall.
+ research:
+ fishing_rate: |
+ At recommended level.
+ sugars_total: |
+ 0 g
+ taste: |
+ Mild, and subtly sweet.
+
+  
+ health_benefits: |
+ Squid are an excellent source of selenium, riboflavin, and vitamin B12.
+ disease_treatment_and_prevention:
+ species_illustration_photo:
+ src: |
+ https://www.fishwatch.gov/sites/default/files/Squid\_Illex\_NB\_W.png
+ title: |
+ Shortfin Squid
+ alt: |
+ shortfin squid
+ saturated_fatty_acids_total: |
+ 0.358 g
+ quote: |
+ U.S. wild-caught shortfin squid is a smart seafood choice because it is
+ sustainably managed and responsibly harvested under U.S. regulations.
+ carbohydrate: |
+ 3.08 g
+ serving_weight: |
+ 100 g
+ ecosystem_services:
+ source: |
+ U.S. wild-caught from Maine to North Carolina.
+ noaa_fisheries_region: |
+ Greater Atlantic
+ animal_health:
+ population_status: |
+ - According to the latest assessment, shortfin squid is not subject to
+ overfishing. There is currently not enough information to determine
+ the population size, so it is unknown.
+ population: |
+ The population level is unknown. The species has a lifespan of less than
+ one year.
+ protein: |
+ 15.58 g
+ environmental_effects:
+  
+ cholesterol: |
+ 233 mg
+ displayed_seafood_profile_illustration:
+ fiber_total_dietary: |
+ 0 g
+ sodium: |
+ 44 mg
+ feeds:
+ servings: |
+ 1
+- calories: 90
+ human_health:
+ texture: The meat is firm and somewhat fibrous. The tail meat is firmer than the
+ meat from the claws.
+ environmental_considerations:
+ species_name: American Lobster
+ diseases_in_salmon:
+ path: |
+ /profiles/american-lobster
+ habitat_impacts: |
+ Fishing gears used to harvest American lobster have minimal impacts on
+ habitat.
+ location: |
+ - American lobsters are found in the northwest Atlantic Ocean from
+ Labrador to Cape Hatteras. They’re most abundant in coastal waters
+ from Maine through New Jersey, and are also common offshore to
+ depths of 2,300 feet from Maine through North Carolina.
+ color: |
+ The meat is white with red tinges.
+ species_aliases: |
+ [Lobster](/species-aliases/lobster)
+ image_gallery:
+ - src: |
+ https://www.fishwatch.gov/sites/default/files/1.JPG
+ title: |
+ American Lobster
+ alt: |
+ American Lobster
+ - src: |
+ https://www.fishwatch.gov/sites/default/files/2\_6.jpg
+ title: |
+ American Lobster
+ alt: |
+ American Lobster
+ - src: |
+ https://www.fishwatch.gov/sites/default/files/3\_5.jpg
+ title: |
+ American Lobster
+ alt: |
+ American Lobster
+ - src: |
+ https://www.fishwatch.gov/sites/default/files/4\_0.png
+ title: |
+ American Lobster
+ alt: |
+ American Lobster
+ - src: |
+ https://www.fishwatch.gov/sites/default/files/5\_3.jpg
+ title: |
+ American Lobster
+ alt: |
+ American Lobster
+ harvest_type: Wild
+ selenium: 41.4 mcg
+ management:
+ scientific_name: Homarus americanus
+ production:
+ fat_total: 0.9 g
+ bycatch: |
+ Regulations are in place to minimize bycatch.
+ availability: |
+ Year-round. In New England, where most lobsters are landed, the peak
+ harvest season extends from May to November.
+ research: |
+ - State scientists, in cooperation with the lobster industry, are
+ conducting projects to assist with the effective management of the
+ lobster resource. Many states have established [ventless trap
+ survey](http://www.asmfc.org/fisheries-science/surveys)s to quantify
+ the abundance of juvenile lobsters. By removing escape vents from
+ the lobster traps and randomly placing those traps within certain
+ depth categories and geographic areas, researchers can assess the
+ abundance of juvenile lobsters and the potential for young lobsters
+ to reach a size or life stage that can be caught by the fishing gear
+ (recruitment) in the future. These surveys complement longstanding
+ fishery-independent bottom trawl surveys conducted by NOAA Fisheries
+ and the states. Because trawl gear cannot effectively sample rocky
+ or shallow coastal bottom types, the ventless trap surveys attempt
+ to fill this data gap by using fixed lobster gear without escape
+ vents.
+ fishing_rate: |
+ At recommended levels.
+ sugars_total: |
+ 0 g
+ taste: |
+ Mild and sweet.
+ health_benefits: |
+ Lobster is low in saturated fat and is a very good source of protein and
+ selenium. The FDA
+ [advises](https://www.fda.gov/downloads/food/guidanceregulation/ucm252395.pdf)
+ consumers to not eat the tomalley, the light-green substance found in
+ the lobster.
+ disease_treatment_and_prevention:
+ species_illustration_photo:
+ src: |
+ https://www.fishwatch.gov/sites/default/files/Lobster\_American\_NB\_Web.png
+ title: |
+ American Lobster
+ alt: |
+ American Lobster
+ saturated_fatty_acids_total: |
+ 0.18 g
+ quote: |
+ U.S. wild-caught American lobster is a smart seafood choice because it
+ is sustainably managed and responsibly harvested under U.S. regulations.
+ carbohydrate: |
+ 0.5 g
+ serving_weight: |
+ 100 g (raw)
+ ecosystem_services:
+ source: |
+ U.S. wild-caught from Maine to North Carolina.
+ noaa_fisheries_region: |
+ Greater Atlantic
+ animal_health:
+ population_status: |
+ - According to the [2015 stock
+ assessment](http://www.asmfc.org/uploads/file/55d61d73AmLobsterStockAssmt_PeerReviewReport_Aug2015_red2.pdf)
+ conducted by the [Atlantic States Marine Fisheries
+ Commission](http://www.asmfc.org/) (ASMFC), there is record high
+ stock abundance and recruitment in the Gulf of Maine and Georges
+ Bank, and record low abundance and recruitment failure in Southern
+ New England. The Gulf of Maine and Georges Bank stock is not
+ overfished. However, the ASMFC considers the Southern New England
+ stock severely depleted due to environmental factors and fishing
+ pressure. Neither stock is subject to overfishing.
+ - Since 2012, [Young of Year
+ surveys](http://umaine.edu/wahlelab/american-lobster-settlement-index-alsi/american-lobster-settlement-index/)
+ in the Gulf of Maine and George’s Bank stock have shown consistent
+ declines, which could indicate future declines in recruitment and
+ landings.
+ population: |
+ Above target population levels in the Gulf of Maine and Georges Bank.
+ Significantly below target levels in Southern New England.
+ protein: |
+ 18.80 g
+ environmental_effects:
+ cholesterol: |
+ 95 mg
+ displayed_seafood_profile_illustration:
+ fiber_total_dietary: |
+ 0 g
+ sodium: |
+ 296 mg
+ feeds:
+ servings: |
+ 1
+- calories: 90
+ human_health:
+ texture: Very lean with medium to firm texture and medium sized flakes.
+ environmental_considerations:
+ species_name: Yellowtail rockfish
+ diseases_in_salmon:
+ path: |
+ /profiles/yellowtail-rockfish
+ habitat_impacts: |
+ Most fishing gear used to harvest yellowtail rockfish rarely contacts
+ the ocean floor and has minimal impacts on habitat. Area closures and
+ gear restrictions protect sensitive rocky, cold-water coral and sponge
+ habitats from bottom trawl gear.
+ location: |
+ - Yellowtail rockfish are found along the Pacific coast of North
+ America and range from Kodiak Island, Alaska to Baja California,
+ Mexico.
+ color: |
+ Meat is glistening bright white with a pinkish sheen.
+ species_aliases: |
+ [Yellowtail rockfish](/species-aliases/yellowtail-rockfish),
+ [Greenie](/species-aliases/greenie), [Yellow sea
+ perch](/species-aliases/yellow-sea-perch), [Rock
+ Cod](/species-aliases/rock-cod), [Pacific
+ Snapper](/species-aliases/pacific-snapper)
+ image_gallery:
+ harvest_type: Wild
+ selenium: 63 mcg
+ management:
+ scientific_name: Sebastes flavidus
+ production:
+ fat_total: 1.34 g
+ bycatch: |
+ Regulations are in place to minimize bycatch of overfished and protected
+ species.
+ availability: |
+ Year-round.
+ research: |
+ - NOAA’s [Northwest](https://www.nwfsc.noaa.gov/) and [Alaska](Alaska)
+ Fisheries Science Centers survey the abundance of yellowtail
+ rockfish off the West Coast and Alaska.
+ - Yellowtail rockfish is not typically assessed as part of a
+ single-species abundance survey. It is more commonly assessed along
+ with other groundfish.
+ fishing_rate: |
+ At recommended levels.
+ sugars_total: |
+ 0
+ taste: |
+ Very mild, slightly sweet flavor.
+ health_benefits: |
+ Rockfish are high in selenium.
+ disease_treatment_and_prevention:
+ species_illustration_photo:
+ src: |
+ https://www.fishwatch.gov/sites/default/files/Rockfish\_Yellowtail\_NB\_W.png
+ title: |
+ Yellowtail rockfish
+ alt: |
+ Yellowtail rockfish
+ saturated_fatty_acids_total: |
+ 0.34 g
+ quote: |
+ U.S. wild-caught Yellowtail rockfish is a smart seafood choice because
+ it is sustainably managed and responsibly harvested under U.S.
+ regulations.
+ carbohydrate: |
+ 0
+ serving_weight: |
+ 100 g (raw)
+ ecosystem_services:
+ source: |
+ U.S. wild-caught from Kodiak Island Alaska to Baja California.
+ noaa_fisheries_region: |
+ West Coast, Alaska
+ animal_health:
+ population_status: |
+ - According to the [2017 stock
+ assessment](https://www.pcouncil.org/wp-content/uploads/2018/01/YTRK_2017_Final.pdf),
+ the northern Pacific coast stock of yellowtail rockfish is not
+ overfished and not subject to overfishing.
+ - The yellowtail rockfish stock on the West Coast is part of the
+ southern Pacific coast minor shelf rockfish complex. The overfished
+ status of this complex is unknown. The stock complex is not subject
+ to overfishing based on [2016 catch
+ data](https://www.nwfsc.noaa.gov/research/divisions/fram/observation/pdf/Groundfish_Mortality_2016.pdf).
+ population: |
+ The northern Pacific coast stock is above its target population level.
+ The southern Pacific coast stock is unknown.
+ protein: |
+ 18.36 g
+ environmental_effects:
+ cholesterol: |
+ 50 mg
+ displayed_seafood_profile_illustration:
+ fiber_total_dietary: |
+ 0
+ sodium: |
+ 74 mg
+ feeds:
+ servings: |
+ 1
+- calories: 90
+ human_health:
+ texture: Lean and medium-firm, with a fine flake.
+ environmental_considerations:
+ species_name: Bocaccio
+ diseases_in_salmon:
+ path: |
+ /profiles/bocaccio
+ habitat_impacts: |
+ Area closures and gear restrictions protect sensitive rocky, cold-water
+ coral and sponge habitats from bottom trawl gear.
+ location: |
+ - Bocaccio are found between Punta Blanca, Baja California, and the
+ Gulf of Alaska off Krozoff and Kodiak Islands. Within this range,
+ bocaccio is most common between Oregon and northern Baja California.
+ - There are two partially isolated populations; one southern
+ population centered in California, and one northern population
+ centered in British Columbia. 
+ color: |
+ Whole fish should have shiny and bright skin. The raw flesh is white,
+ but turns opaque white when cooked.
+ species_aliases: |
+ [Bocaccio](/species-aliases/bocaccio), [Rock
+ Salmon](/species-aliases/rock-salmon), [Salmon
+ Rockfish](/species-aliases/salmon-rockfish), [Pacific Red
+ Snapper](/species-aliases/pacific-red-snapper), [Pacific
+ Snapper](/species-aliases/pacific-snapper), [Oregon Red
+ Snapper](/species-aliases/oregon-red-snapper), [Oregon
+ Snapper](/species-aliases/oregon-snapper),
+ [Longjaw](/species-aliases/longjaw), [Merou](/species-aliases/merou),
+ [Jack](/species-aliases/jack), [Snapper](/species-aliases/snapper),
+ [Rock Cod](/species-aliases/rock-cod),
+ [Rockfish](/species-aliases/rockfish)
+ image_gallery:
+ harvest_type: Wild
+ selenium: 63 mcg
+ management:
+ scientific_name: Sebastes paucispinis
+ production:
+ fat_total: 1.34 g
+ bycatch: |
+ Regulations are in place to minimize bycatch.
+ availability: |
+ Year-round.
+ research: |
+ - [New Fishing Opportunities Emerge from Resurgence of West Coast
+ Groundfish](https://www.fisheries.noaa.gov/feature-story/new-fishing-opportunities-emerge-resurgence-west-coast-groundfish)
+ - [Rebuilding success continues for West Coast
+ groundfish](https://www.westcoast.fisheries.noaa.gov/stories/2017/19_06192017_.html)
+ - [Threatened Yelloweye and Endangered Bocaccio in Puget Sound/Georgia
+ Basin](https://www.westcoast.fisheries.noaa.gov/protected_species/rockfish/rockfish_in_puget_sound.html)
+ fishing_rate: |
+ At recommended level.
+ sugars_total: |
+ 0
+ taste: |
+ Delicate, nutty, sweet flavor.
+ health_benefits: |
+ Low in saturated fat and very high in selenium, phosphorus, and
+ potassium.
+ disease_treatment_and_prevention:
+ species_illustration_photo:
+ src: |
+ https://www.fishwatch.gov/sites/default/files/Bocaccio\_NB\_W.png
+ title: |
+ Bocaccio rockfish.
+ alt: |
+ Illustration of a Bocaccio rockfish.
+ saturated_fatty_acids_total: |
+ 0.34 g
+ quote: |
+ U.S. wild-caught bocaccio is a smart seafood choice because it is
+ sustainably managed and responsibly harvested under U.S. regulations.
+ carbohydrate: |
+ 0
+ serving_weight: |
+ 100 g (raw)
+ ecosystem_services:
+ source: |
+ U.S. wild-caught from California to Alaska.
+ noaa_fisheries_region: |
+ West Coast, Alaska
+ animal_health:
+ population_status: |
+ - According to the [2018 stock
+ assessment](https://www.pcouncil.org/wp-content/uploads/2018/02/FINAL_2017_Bocaccio_Update_Assessment_February_2_2018.pdf),
+ the bocaccio stock on the southern Pacific coast is not overfished,
+ and is not subject to overfishing. The stock rebuilt in 2017, faster
+ than estimated in the rebuilding plan, due in large part to several
+ strong year classes and an improved understanding of the
+ productivity of this stock.
+ - Along the northern Pacific coast, bocaccio is part of the northern
+ Pacific coast minor shelf rockfish complex and the status of this
+ complex is unknown.
+ - In the Gulf of Alaska, bocaccio is part of the other rockfish
+ complex.
+ - According to the [2017 stock
+ assessment](https://www.afsc.noaa.gov/REFM/Docs/2017/GOAorock.pdf),
+ the status of this complex is unknown.
+ population: |
+ Above target population levels.
+ protein: |
+ 18.36 g
+ environmental_effects:
+ cholesterol: |
+ 50 mg
+ displayed_seafood_profile_illustration:
+ fiber_total_dietary: |
+ 0
+ sodium: |
+ 74 mg
+ feeds:
+ servings: |
+ 1
+- calories: 110
+ human_health:
+ texture: A lean fish with fine-grained, dense meat. When cooked, the meat is firm
+ yet flaky and tender.
+ environmental_considerations:
+ species_name: Atlantic Halibut
+ diseases_in_salmon:
+ path: |
+ /profiles/atlantic-halibut
+ habitat_impacts: |
+ Trawl gear used to harvest Atlantic halibut have minimal or temporary
+ effects on habitat. Area closures and gear restrictions protect
+ sensitive habitats from bottom trawl gear. Hook and line gear has little
+ or no impact on habitat.
+ location: |
+ - Atlantic halibut are found from Labrador and Greenland to Iceland,
+ and from the Barents Sea south to the Bay of Biscay and Virginia.
+ - In U.S. waters, halibut is most common in the Gulf of Maine.
+ color: |
+ Uncooked, white and almost translucent. It should not look dull,
+ yellowish or dried out. When cooked, the meat is white.
+ species_aliases: |
+ [Atlantic halibut](/species-aliases/atlantic-halibut),
+ [Halibut](/species-aliases/halibut)
+ image_gallery:
+ - src: |
+ https://www.fishwatch.gov/sites/default/files/1%20-%20atl\_halibut\_noa.jpg
+ title: |
+ Atlantic halibut face and mouth. Photo credit: NOAA.
+ alt: |
+ Picture of an Atlantic halibut face and mouth.
+ - src: |
+ https://www.fishwatch.gov/sites/default/files/2%20-%20nefsc.jpg
+ title: |
+ Atlantic halibut. Photo credit: NOAA.
+ alt: |
+ Picture of Atlantic halibut.
+ - src: |
+ https://www.fishwatch.gov/sites/default/files/3%20-%20halibut2\_fullsize.jpg
+ title: |
+ Picture of Atlantic halibut. Photo credit: NOAA.
+ alt: |
+ Picture of Atlantic halibut.
+ harvest_type: Wild
+ selenium: 36.5 mcg
+ management:
+ scientific_name: Hippoglossus hippoglossus
+ production:
+ fat_total: 2.29 g
+ bycatch: |
+ Regulations are in place to minimize bycatch.
+ availability: |
+ Year-round.
+ research: |
+ - Scientists at NOAA’s [Northeast Fisheries Science
+ Center](https://www.nefsc.noaa.gov/) conduct research bottom trawl
+ surveys throughout the Northeast continental shelf every year during
+ the fall and spring. These surveys collect data on the environment
+ as well as biological samples from fish caught during research
+ trawling. The data from these and other sources are used by
+ scientists in stock assessments to estimate population size and
+ fishing pressure.
+ fishing_rate: |
+ At recommended levels.
+ sugars_total: |
+ 0
+ taste: |
+ Halibut has a very mild, sweet taste.
+ health_benefits: |
+ Halibut is low in saturated fat and sodium, and is a very good source of
+ protein, niacin, phosphorus, and selenium.
+ disease_treatment_and_prevention:
+ species_illustration_photo:
+ src: |
+ https://www.fishwatch.gov/sites/default/files/atlantic-halibut-illustration.png
+ title: |
+ Illustration of Atlantic Halibut.
+ alt: |
+ Illustration of Atlantic Halibut.
+ saturated_fatty_acids_total: |
+ 0.325 g
+ quote: |
+ Although populations are well below target levels, U.S. wild-caught
+ Atlantic halibut is still a smart seafood choice because it is
+ sustainably managed under a rebuilding plan that allows limited harvest
+ by U.S. fishermen.
+ carbohydrate: |
+ 0
+ serving_weight: |
+ 100 g (raw)
+ ecosystem_services:
+ source: |
+ Wild-caught from Maine to Connecticut.
+ noaa_fisheries_region: |
+ Greater Atlantic
+ animal_health:
+ population_status: |
+ - The Atlantic halibut stock is at a very low level. Fishing is still
+ allowed, but at reduced levels.
+ - According to the [2012 stock
+ assessment](https://www.nefsc.noaa.gov/publications/crd/crd1206/),
+ the Atlantic halibut stock is overfished, but is not subject to
+ overfishing. The estimated biomass is only 3 percent of its target
+ level. It will remain in a rebuilding plan for the foreseeable
+ future.
+ population: |
+ Significantly below target population levels.
+ protein: |
+ 20.81 g
+ environmental_effects:
+ cholesterol: |
+ 32 mg
+ displayed_seafood_profile_illustration:
+ fiber_total_dietary: |
+ 0
+ sodium: |
+ 54 mg
+ feeds:
+ servings: |
+ 1
+- calories: 90
+ human_health:
+ texture: Firm, coarse flake.
+ environmental_considerations:
+ species_name: Shortspine Thornyhead
+ diseases_in_salmon:
+ path: |
+ /profiles/shortspine-thornyhead
+ habitat_impacts: |
+ The trawl, longline, and pot gear used to harvest shortspine thornyhead
+ have minimal or temporary effects on habitat. Area closures and gear
+ restrictions protect sensitive rocky, cold-water coral, and sponge
+ habitats from bottom trawl gear.
+ location: |
+ - Shortspine thornyhead are found from the Bering Sea to Baja
+ California, Mexico.
+ color: |
+ White.
+ species_aliases: |
+ [Thornyhead](/species-aliases/thornyhead), [Idiot
+ fish](/species-aliases/idiot-fish), [Idiot
+ cod](/species-aliases/idiot-cod), [Rockfish](/species-aliases/rockfish)
+ image_gallery:
+ - src: |
+ https://www.fishwatch.gov/sites/default/files/1%20photo-west-coast-region-photo-gallery.jpg
+ title: |
+ Close-up photo of a shortspine thornyhead. (Photo credit: NOAA)
+ alt: |
+ Close-up photo of a shortspine thornyhead. (Photo credit: NOAA)
+ - src: |
+ https://www.fishwatch.gov/sites/default/files/2%20basket%20of%20shortspine%20thornyhead.jpg
+ title: |
+ Basket of shortspine thornyhead. (Photo credit: NOAA)
+ alt: |
+ Basket of shortspine thornyhead. (Photo credit: NOAA)
+ - src: |
+ https://www.fishwatch.gov/sites/default/files/3%20graphic%20with%20morphology%20ID\_large.jpg
+ title: |
+ Shortspine thornyhead graphic identifying several physical
+ characteristics, including head spines, pelvic and anal fins. (Photo
+ credit: NOAA)
+ alt: |
+ Shortspine thornyhead graphic identifying several physical
+ characteristics, including head spines, pelvic and anal fins. (Photo
+ credit: NOAA)
+ - src: |
+ https://www.fishwatch.gov/sites/default/files/4%20Head-on%20view%20of%20shortspine%20thornyhead.jpg
+ title: |
+ Head-on view of shortspine thornyhead. (Photo credit: NOAA)
+ alt: |
+ Head-on view of shortspine thornyhead. (Photo credit: NOAA)
+ - src: |
+ https://www.fishwatch.gov/sites/default/files/5%20shortspine%20thornyhead%20amongst%20its%20habitat.jpg
+ title: |
+ Shortspine thornyhead rockfish snuggled amongst a sea star, smaller
+ brittle stars, and sea cucumbers with white tentacles on a mixed rocky
+ and mud-covered habitat. (Photo credit: NOAA/OER)
+ alt: |
+ Shortspine thornyhead rockfish snuggled amongst a sea star, smaller
+ brittle stars, and sea cucumbers with white tentacles on a mixed rocky
+ and mud-covered habitat. (Photo credit: NOAA/OER)
+ harvest_type: Wild
+ selenium: 63 mcg
+ management:
+ scientific_name: Sebastolobus alascanus
+ production:
+ fat_total: 1.34 g
+ bycatch: |
+ Regulations are in place to minimize bycatch of overfished and protected
+ species.
+ availability: |
+ Year-round.
+ research: |
+ [Tagging study of shortspine thornyhead in
+ Alaska](http://agris.fao.org/agris-search/search.do?recordID=US201700202112)
+ confirms that the management range is appropriate.
+ fishing_rate: |
+ At recommended levels.
+ sugars_total: |
+ 0
+ taste: |
+ Sweet and mild.
+ health_benefits: |
+ Rockfish are high in selenium.
+ disease_treatment_and_prevention:
+ species_illustration_photo:
+ src: |
+ https://www.fishwatch.gov/sites/default/files/shortspine-thornyhead-illustration.png
+ title: |
+ Illustration of shortspine thornyhead.
+ alt: |
+ Illustration of shortspine thornyhead.
+ saturated_fatty_acids_total: |
+ 0.34 g
+ quote: |
+ U.S. wild-caught shortspine thornyhead is a smart seafood choice because
+ it is sustainably managed and responsibly harvested under U.S.
+ regulations.
+ carbohydrate: |
+ 0
+ serving_weight: |
+ 100 g (raw)
+ ecosystem_services:
+ source: |
+ U.S. wild-caught from the Bering Sea to Baja California, Mexico.
+ noaa_fisheries_region: |
+ West Coast
+ animal_health:
+ population_status: |
+ - According to the [2013 stock
+ assessment](https://www.pcouncil.org/wp-content/uploads/Shortspine_2013_Assessment.pdf),
+ shortspine thornyhead on the Pacific Coast are not overfished and
+ are not subject to overfishing based on the [2016 catch
+ data](https://www.nwfsc.noaa.gov/research/divisions/fram/observation/pdf/Groundfish_Mortality_2016.pdf).
+ - In the Gulf of Alaska, shortspine thornyhead are part of the
+ thornyhead rockfish complex, which also contains longspine and
+ broadfin thornyhead.
+ - According to the [2018 stock
+ assessment](https://www.fisheries.noaa.gov/resource/data/2018-assessment-thornyhead-stock-complex-gulf-alaska),
+ the status of this complex is unknown.
+ - According to the 2017 catch data, the complex was not subject to
+ overfishing, and the fishery’s total allowable catch has not
+ been attained since 1995.
+ - In the Bering Sea and Aleutian Islands, shortspine thornyhead are
+ part of the other rockfish complex.
+ - According to the [2018 stock
+ assessment](https://www.fisheries.noaa.gov/resource/data/2018-assessment-other-rockfish-stock-complex-bering-sea-and-aleutian-islands),
+ the status of this complex is unknown.
+ - According to the 2017 catch data, the complex was not subject to
+ overfishing.
+ population: |
+ Above target population levels on the Pacific Coast.
+ protein: |
+ 18.36 g
+ environmental_effects:
+ cholesterol: |
+ 50 mg
+ displayed_seafood_profile_illustration:
+ fiber_total_dietary: |
+ 0
+ sodium: |
+ 74 mg
+ feeds:
+ servings: |
+ 1
+...
diff --git a/wasm/index.html b/wasm/index.html
new file mode 100644
index 000000000..6271605f8
--- /dev/null
+++ b/wasm/index.html
@@ -0,0 +1,2486 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>Pandoc in the browser</title>
+ <script src="https://unpkg.com/petite-vue" defer></script>
+ <script type="module">
+ import { TarReader, TarWriter } from 'https://esm.sh/@gera2ld/[email protected]';
+ window.tarjs = { TarReader, TarWriter };
+ </script>
+ <style>
+ :root {
+ --primary: #2563eb;
+ --primary-hover: #1d4ed8;
+ --success: #16a34a;
+ --warning: #d97706;
+ --danger: #dc2626;
+ --gray-50: #f9fafb;
+ --gray-100: #f3f4f6;
+ --gray-200: #e5e7eb;
+ --gray-300: #d1d5db;
+ --gray-400: #9ca3af;
+ --gray-500: #6b7280;
+ --gray-600: #4b5563;
+ --gray-700: #374151;
+ --gray-800: #1f2937;
+ --gray-900: #111827;
+ --radius: 8px;
+ --shadow: 0 1px 3px rgba(0,0,0,0.1), 0 1px 2px rgba(0,0,0,0.06);
+ --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
+ }
+
+ * { box-sizing: border-box; }
+
+ body {
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
+ background: var(--gray-100);
+ color: var(--gray-800);
+ margin: 0;
+ padding: 0;
+ line-height: 1.5;
+ }
+
+ .header {
+ background: var(--gray-900);
+ color: white;
+ padding: 0.5rem 1.5rem;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ }
+
+ .header h1 { margin: 0; font-size: 1.1rem; font-weight: 600; }
+ .header .tagline { color: var(--gray-400); font-size: 0.8rem; }
+
+ .container { max-width: 1200px; margin: 0 auto; padding: 1.5rem; }
+
+ .card {
+ background: white;
+ border-radius: var(--radius);
+ box-shadow: var(--shadow);
+ margin-bottom: 1rem;
+ overflow: hidden;
+ }
+
+ .card-header {
+ background: var(--gray-50);
+ border-bottom: 1px solid var(--gray-200);
+ padding: 1rem 1.5rem;
+ font-weight: 600;
+ font-size: 1rem;
+ display: flex;
+ align-items: center;
+ gap: 0.5rem;
+ }
+
+ .card-body { padding: 1rem; }
+
+ .drop-zone {
+ border: 2px dashed var(--gray-300);
+ border-radius: var(--radius);
+ padding: 1rem;
+ text-align: center;
+ cursor: pointer;
+ transition: all 0.2s;
+ background: var(--gray-50);
+ height: 120px;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ }
+
+ .drop-zone:hover, .drop-zone.drag-over {
+ border-color: var(--primary);
+ background: #eff6ff;
+ }
+
+ .drop-zone-icon { font-size: 2rem; margin-bottom: 0.5rem; opacity: 0.5; }
+ .drop-zone-text { color: var(--gray-600); margin-bottom: 0.25rem; font-size: 0.9rem; }
+ .drop-zone-hint { color: var(--gray-400); font-size: 0.8rem; }
+
+ .file-list { margin-top: 1rem; }
+
+ .file-item {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.75rem 1rem;
+ background: var(--gray-50);
+ border-radius: var(--radius);
+ margin-bottom: 0.5rem;
+ cursor: grab;
+ user-select: none;
+ }
+
+ .file-item:active { cursor: grabbing; }
+ .file-item.dragging { opacity: 0.5; }
+ .file-item.drag-over { border-top: 2px solid var(--primary); margin-top: -2px; }
+ .file-item .file-icon { font-size: 1.25rem; }
+ .file-item .file-name { flex: 1; font-size: 0.9rem; word-break: break-all; }
+ .file-item a.file-name { color: var(--primary); text-decoration: none; cursor: pointer; }
+ .file-item a.file-name:hover { text-decoration: underline; }
+ .resource-item a { color: var(--primary); text-decoration: none; cursor: pointer; }
+ .resource-item a:hover { text-decoration: underline; }
+ .file-item .file-size { color: var(--gray-500); font-size: 0.8rem; }
+
+ .file-item .file-remove {
+ color: var(--gray-400);
+ cursor: pointer;
+ padding: 0.25rem;
+ border-radius: 4px;
+ transition: all 0.2s;
+ }
+
+ .file-item .file-remove:hover { color: var(--danger); background: #fef2f2; }
+
+ .auxiliary-section { margin-top: 0.75rem; padding-top: 0.75rem; border-top: 1px solid var(--gray-200); }
+ .auxiliary-header { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.25rem; }
+ .auxiliary-label { font-size: 0.8rem; color: var(--gray-600); }
+
+ .upload-btn-small {
+ padding: 0.35rem 0.75rem;
+ font-size: 0.8rem;
+ background: var(--gray-100);
+ border: 1px solid var(--gray-300);
+ border-radius: var(--radius);
+ cursor: pointer;
+ transition: all 0.2s;
+ }
+
+ .upload-btn-small:hover { background: var(--gray-200); }
+
+ .file-list-small .file-item { padding: 0.5rem 0.75rem; }
+ .file-list-small .file-item .file-icon { font-size: 1rem; }
+ .file-list-small .file-item .file-name { font-size: 0.85rem; }
+
+ .input-mode-toggle {
+ display: inline-flex;
+ background: var(--gray-100);
+ border-radius: var(--radius);
+ padding: 3px;
+ }
+
+ .input-mode-btn {
+ padding: 0.35rem 0.75rem;
+ border: none;
+ background: transparent;
+ border-radius: calc(var(--radius) - 2px);
+ cursor: pointer;
+ font-size: 0.85rem;
+ transition: all 0.2s;
+ color: var(--gray-600);
+ }
+
+ .input-mode-btn:hover:not(.active) { color: var(--gray-800); }
+
+ .input-mode-btn.active {
+ background: white;
+ color: var(--gray-900);
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
+ }
+
+ .text-input-area textarea {
+ width: 100%;
+ height: 120px;
+ padding: 1rem;
+ border: 1px solid var(--gray-300);
+ border-radius: var(--radius);
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 0.9rem;
+ resize: vertical;
+ }
+
+ .input-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 0.75rem;
+ flex-wrap: wrap;
+ }
+
+ .input-controls { display: flex; align-items: center; gap: 0.75rem; }
+
+ .format-bar { display: flex; align-items: center; gap: 1rem; flex-wrap: wrap; }
+ .format-bar .form-group { flex-direction: row; align-items: center; gap: 0.5rem; }
+ .format-bar .form-group label { white-space: nowrap; }
+ .format-bar .form-group select { padding: 0.5rem 0.75rem; font-size: 0.85rem; width: 10em; }
+ .format-bar .form-group input[type="text"] { padding: 0.5rem 0.75rem; font-size: 0.85rem; width: 140px; }
+
+ .examples-toggle { margin-bottom: 0.5rem; }
+ .examples-toggle a { font-size: 0.85rem; color: var(--gray-600); text-decoration: none; }
+ .examples-toggle a:hover { color: var(--primary); }
+ .examples-bar { display: flex; align-items: center; gap: 0.5rem; }
+ .examples-bar label { font-size: 0.8rem; color: var(--gray-500); font-weight: 500; }
+ .examples-bar select { padding: 0.25rem 0.5rem; font-size: 0.8rem; border: 1px solid var(--gray-300); border-radius: 4px; background: white; }
+
+ .extensions-row { display: flex; gap: 2rem; flex-wrap: wrap; }
+ .extensions-group { flex: 1; min-width: 200px; }
+ .extensions-group-header {
+ font-size: 0.75rem;
+ font-weight: 600;
+ color: var(--gray-500);
+ margin-bottom: 0.35rem;
+ text-transform: uppercase;
+ letter-spacing: 0.03em;
+ }
+
+ .extensions-list {
+ columns: 3;
+ column-gap: 1rem;
+ max-height: 200px;
+ overflow-y: auto;
+ }
+
+ .extension-item {
+ display: flex;
+ align-items: center;
+ gap: 0.2rem;
+ font-size: 0.8rem;
+ break-inside: avoid;
+ }
+
+ .extension-item input[type="checkbox"] { width: 0.85rem; height: 0.85rem; }
+ .extension-item label { cursor: pointer; color: var(--gray-600); }
+ .extension-item.default-on label { font-weight: 500; }
+ .extension-item.toggled label { color: var(--primary); }
+
+ .custom-metadata { margin-top: 0.5rem; }
+
+ .custom-meta-row {
+ display: flex;
+ gap: 0.5rem;
+ align-items: center;
+ margin-bottom: 0.5rem;
+ }
+
+ .custom-meta-row input[type="text"] {
+ padding: 0.5rem 0.75rem;
+ border: 1px solid var(--gray-300);
+ border-radius: var(--radius);
+ font-size: 0.9rem;
+ }
+
+ .custom-meta-row input[name="key"] { width: 120px; }
+ .custom-meta-row input[name="value"] { flex: 1; }
+
+ .custom-meta-row .remove-meta {
+ padding: 0.25rem 0.5rem;
+ background: none;
+ border: none;
+ color: var(--gray-400);
+ cursor: pointer;
+ font-size: 1rem;
+ }
+
+ .custom-meta-row .remove-meta:hover { color: var(--danger); }
+
+ .add-metadata-btn {
+ margin-top: 0.5rem;
+ padding: 0.35rem 0.75rem;
+ font-size: 0.8rem;
+ background: var(--gray-100);
+ border: 1px solid var(--gray-300);
+ border-radius: var(--radius);
+ cursor: pointer;
+ color: var(--gray-600);
+ }
+
+ .add-metadata-btn:hover { background: var(--gray-200); }
+
+ .variables-section { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--gray-200); }
+ .variables-section > label {
+ display: block;
+ font-size: 0.9rem;
+ font-weight: 500;
+ color: var(--gray-700);
+ margin-bottom: 0.5rem;
+ }
+
+ .form-group { display: flex; flex-direction: column; gap: 0.5rem; }
+ .form-group label { font-weight: 500; font-size: 0.9rem; color: var(--gray-700); }
+
+ .form-group select, .form-group input[type="text"] {
+ padding: 0.75rem 1rem;
+ border: 1px solid var(--gray-300);
+ border-radius: var(--radius);
+ font-size: 0.95rem;
+ background: white;
+ transition: border-color 0.2s;
+ }
+
+ .form-group select:focus, .form-group input[type="text"]:focus {
+ outline: none;
+ border-color: var(--primary);
+ box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
+ }
+
+ .form-group .hint { font-size: 0.8rem; color: var(--gray-500); }
+
+ .options-tabs {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 0.25rem;
+ border-bottom: 1px solid var(--gray-200);
+ margin-bottom: 0.75rem;
+ }
+
+ .options-tab {
+ padding: 0.4rem 0.6rem;
+ font-size: 0.85rem;
+ background: none;
+ border: none;
+ border-bottom: 2px solid transparent;
+ cursor: pointer;
+ color: var(--gray-600);
+ transition: all 0.2s;
+ white-space: nowrap;
+ }
+
+ .options-tab:hover { color: var(--gray-800); background: var(--gray-50); }
+ .options-tab.active { color: var(--primary); border-bottom-color: var(--primary); font-weight: 500; }
+
+ .options-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
+ gap: 1rem;
+ }
+
+ #panel-format .options-grid {
+ grid-template-columns: repeat(auto-fill, minmax(165px, max-content));
+ justify-content: start;
+ gap: 0.75rem 1.5rem;
+ }
+
+ #panel-format .form-group select { width: auto; min-width: 6em; }
+
+ .checkbox-group { display: flex; align-items: center; gap: 0.5rem; }
+ .checkbox-group input[type="checkbox"] { width: 1.1rem; height: 1.1rem; cursor: pointer; }
+ .checkbox-group label { font-size: 0.9rem; cursor: pointer; }
+
+ .resource-upload { margin-top: 1rem; }
+
+ .resource-item {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ padding: 0.5rem;
+ background: var(--gray-50);
+ border-radius: var(--radius);
+ margin-bottom: 0.5rem;
+ font-size: 0.85rem;
+ }
+
+ .upload-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.5rem 1rem;
+ background: var(--gray-100);
+ border: 1px solid var(--gray-300);
+ border-radius: var(--radius);
+ cursor: pointer;
+ font-size: 0.85rem;
+ transition: all 0.2s;
+ }
+
+ .upload-btn:hover { background: var(--gray-200); }
+
+ .convert-btn {
+ padding: 0.4rem 1rem;
+ font-size: 0.85rem;
+ font-weight: 600;
+ background: var(--primary);
+ color: white;
+ border: none;
+ border-radius: var(--radius);
+ cursor: pointer;
+ transition: all 0.2s;
+ }
+
+ .convert-btn:hover:not(:disabled) { background: var(--primary-hover); }
+ .convert-btn:disabled { background: var(--gray-400); cursor: not-allowed; }
+
+ .output-preview {
+ background: var(--gray-900);
+ color: #e5e7eb;
+ padding: 1rem;
+ border-radius: var(--radius);
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
+ font-size: 0.85rem;
+ max-height: 400px;
+ overflow: auto;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+ }
+
+ .output-actions { display: flex; gap: 1rem; margin-top: 1rem; }
+
+ .download-btn {
+ display: inline-flex;
+ align-items: center;
+ gap: 0.5rem;
+ padding: 0.75rem 1.5rem;
+ background: var(--success);
+ color: white;
+ border: none;
+ border-radius: var(--radius);
+ cursor: pointer;
+ font-size: 0.95rem;
+ transition: all 0.2s;
+ }
+
+ .download-btn:hover { background: #15803d; }
+
+ .copy-btn {
+ padding: 0.75rem 1.5rem;
+ background: var(--gray-600);
+ color: white;
+ border: none;
+ border-radius: var(--radius);
+ cursor: pointer;
+ font-size: 0.95rem;
+ transition: all 0.2s;
+ }
+
+ .copy-btn:hover { background: var(--gray-700); }
+
+ .messages { margin-top: 1rem; }
+
+ .message {
+ padding: 0.75rem 1rem;
+ border-radius: var(--radius);
+ margin-bottom: 0.5rem;
+ font-size: 0.9rem;
+ }
+
+ .message.error { background: #fef2f2; color: var(--danger); border: 1px solid #fecaca; }
+ .message.warning { background: #fffbeb; color: var(--warning); border: 1px solid #fde68a; }
+ .message.info { background: #eff6ff; color: var(--primary); border: 1px solid #bfdbfe; }
+
+ .loading { text-align: center; padding: 2rem; }
+
+ .spinner {
+ width: 40px;
+ height: 40px;
+ border: 4px solid var(--gray-200);
+ border-top-color: var(--primary);
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin: 0 auto 1rem;
+ }
+
+ button { line-height: 1.1; }
+
+ @keyframes spin { to { transform: rotate(360deg); } }
+
+ .loading-overlay {
+ position: fixed;
+ top: 0; left: 0; right: 0; bottom: 0;
+ background: rgba(255, 255, 255, 0.5);
+ backdrop-filter: blur(4px);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ z-index: 1000;
+ }
+
+ .loading-box {
+ background: white;
+ padding: 2rem 3rem;
+ border-radius: var(--radius);
+ box-shadow: var(--shadow-lg);
+ text-align: center;
+ }
+
+ .loading-box p { margin: 0; color: var(--gray-600); font-size: 0.95rem; }
+
+ @media (max-width: 640px) {
+ .container { padding: 0.75rem; }
+ .input-header { flex-direction: column; align-items: stretch; }
+ .input-controls { justify-content: space-between; }
+ .format-bar { flex-direction: column; align-items: stretch; }
+ .format-bar .form-group { flex-direction: column; align-items: stretch; }
+ .format-bar .form-group input[type="text"] { width: 100%; }
+ .options-grid { grid-template-columns: 1fr; }
+ }
+
+ .hidden { display: none !important; }
+ input[type="file"].hidden { display: none; }
+ </style>
+</head>
+<body>
+ <div class="header">
+ <h1>pandoc.wasm</h1>
+ <span class="tagline">Convert documents without leaving the browser</span>
+ </div>
+
+ <div id="app" v-scope="pandocApp()" @vue:mounted="init()">
+ <div class="container">
+ <!-- Input Section -->
+ <div class="examples-toggle">
+ <a href="#" @click.prevent="showExamplesBar = !showExamplesBar">
+ {{ showExamplesBar ? '&#9660;' : '&#9654;' }} Examples
+ </a>
+ </div>
+ <div class="card" v-show="showExamplesBar">
+ <div class="card-body">
+ <div class="examples-bar">
+ <label for="examples-select">Load:</label>
+ <select id="examples-select" v-model="selectedExample" @change="loadExample">
+ <option value="">Select example...</option>
+ <option value="examples/bibtex-to-csljson.tar">BibTeX to CSL JSON</option>
+ <option value="examples/csv-table-to-org.tar">CSV table to Org</option>
+ <option value="examples/custom-template.tar">Custom template</option>
+ <option value="examples/docx-with-equations-to-latex.tar">DOCX with equations to LaTeX</option>
+ <option value="examples/hello-world.tar">Hello world</option>
+ <option value="examples/highlighted-code-to-html.tar">Highlighted code to HTML</option>
+ <option value="examples/ipynb-to-rtf.tar">Jupyter notebook to RTF</option>
+ <option value="examples/latex-to-docbook-with-mathml.tar">LaTeX to DocBook with MathML</option>
+ <option value="examples/latex-with-macros-to-restructured-text.tar">LaTeX with macros to RST</option>
+ <option value="examples/man-page-to-context.tar">Man page to ConTeXt</option>
+ <option value="examples/markdown-citations-to-plain-with-csl-style.tar">Markdown citations to plain</option>
+ <option value="examples/markdown-to-docbook-with-citations.tar">Markdown to DocBook with citations</option>
+ <option value="examples/markdown-to-revealjs-slides.tar">Markdown to reveal.js slides</option>
+ <option value="examples/markdown-to-rst.tar">Markdown to RST</option>
+ <option value="examples/mediawiki-to-docx-with-equations.tar">MediaWiki to DOCX with equations</option>
+ <option value="examples/ris-to-formatted-markdown-bibliography.tar">RIS to Markdown bibliography</option>
+ <option value="examples/rst-table-from-yaml-data.tar">RST table from YAML data</option>
+ </select>
+ <button class="upload-btn-small" @click="downloadAsExample">Download as example</button>
+ <button class="upload-btn-small" @click.stop="$refs.exampleInput.click()">Upload example</button>
+ <input type="file" ref="exampleInput" class="hidden" accept=".tar" @change.stop="uploadExample($event)" />
+ </div>
+ </div>
+ </div>
+ <div class="card">
+ <div class="card-body">
+ <div class="input-header">
+ <div class="input-controls">
+ <div class="input-mode-toggle">
+ <button class="input-mode-btn" :class="{ active: inputMode === 'file' }" @click="inputMode = 'file'">Upload Files</button>
+ <button class="input-mode-btn" :class="{ active: inputMode === 'text' }" @click="inputMode = 'text'">Paste Text</button>
+ </div>
+ <button class="convert-btn" :disabled="!canConvert" @click="convert">Convert</button>
+ </div>
+ <div class="format-bar">
+ <div class="form-group">
+ <label for="input-format">From:</label>
+ <select id="input-format" v-model="inputFormat" @change="onInputFormatChange">
+ <option value="auto">Auto-detect</option>
+ <option value="asciidoc">asciidoc (AsciiDoc)</option>
+ <option value="biblatex">biblatex (BibLaTeX bibliography)</option>
+ <option value="bibtex">bibtex (BibTeX bibliography)</option>
+ <option value="bits">bits (BITS XML, alias for jats)</option>
+ <option value="commonmark">commonmark (CommonMark Markdown)</option>
+ <option value="commonmark_x">commonmark_x (CommonMark with extensions)</option>
+ <option value="creole">creole (Creole 1.0)</option>
+ <option value="csljson">csljson (CSL JSON bibliography)</option>
+ <option value="csv">csv (CSV table)</option>
+ <option value="djot">djot (Djot markup)</option>
+ <option value="docbook">docbook (DocBook)</option>
+ <option value="docx">docx (Word)</option>
+ <option value="dokuwiki">dokuwiki (DokuWiki markup)</option>
+ <option value="endnotexml">endnotexml (EndNote XML bibliography)</option>
+ <option value="epub">epub (EPUB)</option>
+ <option value="fb2">fb2 (FictionBook2)</option>
+ <option value="gfm">gfm (GitHub-Flavored Markdown)</option>
+ <option value="haddock">haddock (Haddock markup)</option>
+ <option value="html">html (HTML)</option>
+ <option value="ipynb">ipynb (Jupyter notebook)</option>
+ <option value="jats">jats (JATS XML)</option>
+ <option value="jira">jira (Jira/Confluence wiki markup)</option>
+ <option value="json">json (JSON version of native AST)</option>
+ <option value="latex">latex (LaTeX)</option>
+ <option value="man">man (roff man)</option>
+ <option value="markdown">markdown (Pandoc's Markdown)</option>
+ <option value="markdown_mmd">markdown_mmd (MultiMarkdown)</option>
+ <option value="markdown_phpextra">markdown_phpextra (PHP Markdown Extra)</option>
+ <option value="markdown_strict">markdown_strict (original unextended Markdown)</option>
+ <option value="mdoc">mdoc (mdoc manual page markup)</option>
+ <option value="mediawiki">mediawiki (MediaWiki markup)</option>
+ <option value="muse">muse (Muse)</option>
+ <option value="native">native (native Haskell)</option>
+ <option value="odt">odt (OpenDocument text)</option>
+ <option value="opml">opml (OPML)</option>
+ <option value="org">org (Emacs Org mode)</option>
+ <option value="pod">pod (Perl POD)</option>
+ <option value="pptx">pptx (PowerPoint)</option>
+ <option value="ris">ris (RIS bibliography)</option>
+ <option value="rst">rst (reStructuredText)</option>
+ <option value="rtf">rtf (Rich Text Format)</option>
+ <option value="t2t">t2t (txt2tags)</option>
+ <option value="textile">textile (Textile)</option>
+ <option value="tikiwiki">tikiwiki (TikiWiki markup)</option>
+ <option value="tsv">tsv (TSV table)</option>
+ <option value="twiki">twiki (TWiki markup)</option>
+ <option value="typst">typst (Typst)</option>
+ <option value="vimwiki">vimwiki (Vimwiki)</option>
+ <option value="xlsx">xlsx (Excel spreadsheet)</option>
+ <option value="xml">xml (XML version of native AST)</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="output-format">To:</label>
+ <select id="output-format" v-model="outputFormat" @change="onOutputFormatChange">
+ <option value="auto">Auto-detect</option>
+ <option value="ansi">ansi (ANSI terminal)</option>
+ <option value="asciidoc">asciidoc (modern AsciiDoc)</option>
+ <option value="asciidoc_legacy">asciidoc_legacy (AsciiDoc for asciidoc-py)</option>
+ <option value="asciidoctor">asciidoctor (AsciiDoctor)</option>
+ <option value="bbcode">bbcode (BBCode)</option>
+ <option value="beamer">beamer (LaTeX Beamer slides)</option>
+ <option value="biblatex">biblatex (BibLaTeX bibliography)</option>
+ <option value="bibtex">bibtex (BibTeX bibliography)</option>
+ <option value="chunkedhtml">chunkedhtml (zip of linked HTML files)</option>
+ <option value="commonmark">commonmark (CommonMark Markdown)</option>
+ <option value="commonmark_x">commonmark_x (CommonMark with extensions)</option>
+ <option value="context">context (ConTeXt)</option>
+ <option value="csljson">csljson (CSL JSON bibliography)</option>
+ <option value="djot">djot (Djot markup)</option>
+ <option value="docbook">docbook (DocBook 4)</option>
+ <option value="docbook5">docbook5 (DocBook 5)</option>
+ <option value="docx">docx (Word)</option>
+ <option value="dokuwiki">dokuwiki (DokuWiki markup)</option>
+ <option value="dzslides">dzslides (DZSlides HTML slides)</option>
+ <option value="epub">epub (EPUB v3)</option>
+ <option value="epub2">epub2 (EPUB v2)</option>
+ <option value="epub3">epub3 (EPUB v3)</option>
+ <option value="fb2">fb2 (FictionBook2)</option>
+ <option value="gfm">gfm (GitHub-Flavored Markdown)</option>
+ <option value="haddock">haddock (Haddock markup)</option>
+ <option value="html">html (HTML5)</option>
+ <option value="html4">html4 (XHTML 1.0 Transitional)</option>
+ <option value="html5">html5 (HTML5)</option>
+ <option value="icml">icml (InDesign ICML)</option>
+ <option value="ipynb">ipynb (Jupyter notebook)</option>
+ <option value="jats">jats (JATS XML)</option>
+ <option value="jira">jira (Jira/Confluence wiki markup)</option>
+ <option value="json">json (JSON version of native AST)</option>
+ <option value="latex">latex (LaTeX)</option>
+ <option value="man">man (roff man)</option>
+ <option value="markdown">markdown (Pandoc's Markdown)</option>
+ <option value="markdown_mmd">markdown_mmd (MultiMarkdown)</option>
+ <option value="markdown_phpextra">markdown_phpextra (PHP Markdown Extra)</option>
+ <option value="markdown_strict">markdown_strict (original unextended Markdown)</option>
+ <option value="markua">markua (Markua)</option>
+ <option value="mediawiki">mediawiki (MediaWiki markup)</option>
+ <option value="ms">ms (roff ms)</option>
+ <option value="muse">muse (Muse)</option>
+ <option value="native">native (native Haskell)</option>
+ <option value="odt">odt (OpenDocument text)</option>
+ <option value="opendocument">opendocument (OpenDocument XML)</option>
+ <option value="opml">opml (OPML)</option>
+ <option value="org">org (Emacs Org mode)</option>
+ <option value="pdf-typst">pdf (via Typst)</option>
+ <option value="plain">plain (plain text)</option>
+ <option value="pptx">pptx (PowerPoint)</option>
+ <option value="revealjs">revealjs (reveal.js HTML slides)</option>
+ <option value="rst">rst (reStructuredText)</option>
+ <option value="rtf">rtf (Rich Text Format)</option>
+ <option value="s5">s5 (S5 HTML slides)</option>
+ <option value="slideous">slideous (Slideous HTML slides)</option>
+ <option value="slidy">slidy (Slidy HTML slides)</option>
+ <option value="tei">tei (TEI Simple)</option>
+ <option value="texinfo">texinfo (GNU Texinfo)</option>
+ <option value="textile">textile (Textile)</option>
+ <option value="typst">typst (Typst)</option>
+ <option value="vimdoc">vimdoc (Vimdoc)</option>
+ <option value="xml">xml (XML version of native AST)</option>
+ <option value="xwiki">xwiki (XWiki markup)</option>
+ <option value="zimwiki">zimwiki (ZimWiki markup)</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="output-filename">Output:</label>
+ <input type="text" id="output-filename" v-model="outputFilename" :placeholder="outputFilenamePlaceholder" />
+ </div>
+ </div>
+ </div>
+
+ <!-- File input area -->
+ <div v-show="inputMode === 'file'">
+ <div class="drop-zone" @click="$refs.fileInput.click()" @dragover.prevent="dragOver = true" @dragleave="dragOver = false" @drop.prevent="handleDrop($event)" :class="{ 'drag-over': dragOver }">
+ <div class="drop-zone-icon">&#128196;</div>
+ <div class="drop-zone-text">Drop files here or click to browse</div>
+ <div class="drop-zone-hint">Supports: .md, .docx, .html, .tex, .rst, .epub, .odt, and more</div>
+ </div>
+ <input type="file" ref="fileInput" class="hidden" multiple @change="handleFileInput($event)" />
+ <div class="file-list" v-if="fileOrder.length > 0">
+ <div class="file-item" v-for="(name, idx) in fileOrder" :key="name" draggable="true" @dragstart="onFileDragStart(idx, $event)" @dragover.prevent="onFileDragOver(idx)" @dragleave="fileDragOverIdx = -1" @drop.prevent="onFileDrop(idx)" @dragend="fileDraggingIdx = -1" :class="{ dragging: fileDraggingIdx === idx, 'drag-over': fileDragOverIdx === idx && fileDraggingIdx !== idx }">
+ <span class="file-icon">&#128196;</span>
+ <a class="file-name" href="#" @click.prevent.stop="downloadFile(files[name], name)">{{ name }}</a>
+ <span class="file-size">{{ formatFileSize(files[name].size) }}</span>
+ <span class="file-remove" @click.stop="removeFile(name)">&#10005;</span>
+ </div>
+ </div>
+ </div>
+
+ <!-- Text input area -->
+ <div class="text-input-area" v-show="inputMode === 'text'">
+ <textarea v-model="textInput" placeholder="Paste or type your content here..."></textarea>
+ </div>
+
+ <!-- Auxiliary files -->
+ <div class="auxiliary-section">
+ <div class="auxiliary-header">
+ <span class="auxiliary-label">Additional files (images, includes, etc.)</span>
+ <button class="upload-btn-small" @click="$refs.auxFileInput.click()">+ Add Files...</button>
+ <input type="file" ref="auxFileInput" class="hidden" multiple @change="handleAuxFiles($event)" />
+ <button class="upload-btn-small" @click="$refs.auxDirInput.click()">+ Add Directory...</button>
+ <input type="file" ref="auxDirInput" class="hidden" webkitdirectory @change="handleAuxDir($event)" />
+ </div>
+ <div class="file-list file-list-small" v-if="Object.keys(auxFiles).length > 0">
+ <div class="file-item" v-for="(file, name) in auxFiles" :key="name">
+ <span class="file-icon">&#128444;</span>
+ <a class="file-name" href="#" @click.prevent="downloadFile(file, name)">{{ name }}</a>
+ <span class="file-size">{{ formatFileSize(file.size) }}</span>
+ <span class="file-remove" @click="removeAuxFile(name)">&#10005;</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Options -->
+ <div class="card">
+ <div class="card-body" style="padding: 0.75rem;">
+ <div class="options-tabs">
+ <button class="options-tab" :class="{ active: activeTab === 'general' }" @click="activeTab = 'general'">General</button>
+ <button class="options-tab" v-show="showTrackChangesTab" :class="{ active: activeTab === 'track-changes' }" @click="activeTab = 'track-changes'">Track Changes</button>
+ <button class="options-tab" v-show="showExtensionsTab" :class="{ active: activeTab === 'extensions' }" @click="activeTab = 'extensions'">Extensions</button>
+ <button class="options-tab" :class="{ active: activeTab === 'citations' }" @click="activeTab = 'citations'">Citations</button>
+ <button class="options-tab" :class="{ active: activeTab === 'math' }" @click="activeTab = 'math'">Math</button>
+ <button class="options-tab" :class="{ active: activeTab === 'code' }" @click="activeTab = 'code'">Code</button>
+ <button class="options-tab" v-show="showFormatTab" :class="{ active: activeTab === 'format' }" @click="activeTab = 'format'">{{ formatTabName }}</button>
+ <button class="options-tab" v-show="showEpubTab" :class="{ active: activeTab === 'epub' }" @click="activeTab = 'epub'">EPUB</button>
+ <button class="options-tab" v-show="showChunkedTab" :class="{ active: activeTab === 'chunked' }" @click="activeTab = 'chunked'">Chunked</button>
+ <button class="options-tab" v-show="showIpynbTab" :class="{ active: activeTab === 'ipynb' }" @click="activeTab = 'ipynb'">Notebook</button>
+ <button class="options-tab" :class="{ active: activeTab === 'template' }" @click="activeTab = 'template'">Template</button>
+ <button class="options-tab" v-show="showSlidesTab" :class="{ active: activeTab === 'slides' }" @click="activeTab = 'slides'">Slides</button>
+ <button class="options-tab" :class="{ active: activeTab === 'metadata' }" @click="activeTab = 'metadata'">Metadata</button>
+ <button class="options-tab" :class="{ active: activeTab === 'wrapping' }" @click="activeTab = 'wrapping'">Wrapping</button>
+ </div>
+
+ <!-- General -->
+ <div class="tab-panel" v-show="activeTab === 'general'">
+ <div class="options-grid">
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-standalone" v-model="opts.standalone" />
+ <label for="opt-standalone">Standalone document</label>
+ </div>
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-toc" v-model="opts.toc" />
+ <label for="opt-toc">Table of contents</label>
+ </div>
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-number-sections" v-model="opts.numberSections" />
+ <label for="opt-number-sections">Number sections</label>
+ </div>
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-file-scope" v-model="opts.fileScope" />
+ <label for="opt-file-scope">File scope (parse files separately)</label>
+ </div>
+ </div>
+ <div class="options-grid" style="margin-top: 0.75rem;">
+ <div class="form-group">
+ <label for="opt-toc-depth">TOC depth</label>
+ <select id="opt-toc-depth" v-model="opts.tocDepth">
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="3">3</option>
+ <option value="4">4</option>
+ <option value="5">5</option>
+ <option value="6">6</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="opt-shift-heading">Shift heading level by</label>
+ <select id="opt-shift-heading" v-model="opts.shiftHeading">
+ <option value="-2">-2</option>
+ <option value="-1">-1</option>
+ <option value="0">0</option>
+ <option value="1">+1</option>
+ <option value="2">+2</option>
+ </select>
+ </div>
+ </div>
+ </div>
+
+ <!-- Track Changes -->
+ <div class="tab-panel" v-show="activeTab === 'track-changes'">
+ <div class="form-group" style="max-width: 250px;">
+ <label for="opt-track-changes">Handle tracked changes</label>
+ <select id="opt-track-changes" v-model="opts.trackChanges">
+ <option value="">Default (accept)</option>
+ <option value="accept">Accept all changes</option>
+ <option value="reject">Reject all changes</option>
+ <option value="all">Include all (insertions, deletions, comments)</option>
+ </select>
+ </div>
+ <p style="color: var(--gray-500); font-size: 0.85rem; margin-top: 0.75rem;">
+ Controls how tracked changes and comments in Word documents are handled during conversion.
+ </p>
+ </div>
+
+ <!-- Extensions -->
+ <div class="tab-panel" v-show="activeTab === 'extensions'">
+ <div class="extensions-row">
+ <div class="extensions-group" v-show="inputFormat !== 'auto'">
+ <div class="extensions-group-header">From ({{ inputFormat }})</div>
+ <div class="extensions-list">
+ <div class="extension-item" v-for="ext in inputExtensionsList" :key="'in-' + ext.name" :class="{ 'default-on': ext.defaultOn, toggled: inputExtensions[ext.name] !== undefined }">
+ <input type="checkbox" :id="'ext-in-' + ext.name" :checked="getExtensionChecked('input', ext)" @change="toggleExtension('input', ext, $event)" />
+ <label :for="'ext-in-' + ext.name">{{ ext.name }}</label>
+ </div>
+ </div>
+ </div>
+ <div class="extensions-group" v-show="outputFormat !== 'auto'">
+ <div class="extensions-group-header">To ({{ effectiveOutputFormat }})</div>
+ <div class="extensions-list">
+ <div class="extension-item" v-for="ext in outputExtensionsList" :key="'out-' + ext.name" :class="{ 'default-on': ext.defaultOn, toggled: outputExtensions[ext.name] !== undefined }">
+ <input type="checkbox" :id="'ext-out-' + ext.name" :checked="getExtensionChecked('output', ext)" @change="toggleExtension('output', ext, $event)" />
+ <label :for="'ext-out-' + ext.name">{{ ext.name }}</label>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Citations -->
+ <div class="tab-panel" v-show="activeTab === 'citations'">
+ <div class="form-group" style="max-width: 200px;">
+ <label for="opt-citation-method">Citation processing</label>
+ <select id="opt-citation-method" v-model="opts.citationMethod">
+ <option value="">None</option>
+ <option value="citeproc">Citeproc</option>
+ <option v-if="isLatexLikeFormat" value="natbib">Natbib</option>
+ <option v-if="isLatexLikeFormat" value="biblatex">BibLaTeX</option>
+ </select>
+ </div>
+ <div class="resource-upload" v-show="opts.citationMethod">
+ <label>Bibliography file (.bib, .json, .yaml)</label>
+ <input type="file" ref="bibInput" class="hidden" accept=".bib,.bibtex,.json,.yaml,.yml" @change="handleBibFile($event)" />
+ <button class="upload-btn" @click="$refs.bibInput.click()">Upload Bibliography</button>
+ <div v-if="bibFile" class="resource-item">&#128196; <a href="#" @click.prevent="downloadFile(bibFile, bibFile.name)">{{ bibFile.name }}</a> <span class="file-remove" @click="bibFile = null">&#10005;</span></div>
+ </div>
+ <div v-show="opts.citationMethod === 'citeproc'">
+ <div class="resource-upload">
+ <label>Citation style (.csl)</label>
+ <div class="options-grid" style="margin-top: 0.5rem;">
+ <div>
+ <input type="file" ref="cslInput" class="hidden" accept=".csl" @change="handleCslFile($event)" />
+ <button class="upload-btn" @click="$refs.cslInput.click()">Upload CSL Style</button>
+ </div>
+ <div style="display: flex; gap: 0.5rem; align-items: flex-start;">
+ <input type="text" v-model="cslStyleName" list="csl-styles-list" placeholder="Or select/type style name..." style="flex: 1;" @focus="loadCslStyles" />
+ <button class="upload-btn" @click="fetchCslStyle">Fetch</button>
+ </div>
+ </div>
+ <datalist id="csl-styles-list">
+ <option v-for="style in cslStylesList" :key="style" :value="style"></option>
+ </datalist>
+ <div v-if="cslFile" class="resource-item">&#128196; <a href="#" @click.prevent="downloadFile(cslFile, cslFile.name)">{{ cslFile.name }}</a> <span class="file-remove" @click="cslFile = null">&#10005;</span></div>
+ </div>
+ <div class="resource-upload">
+ <label>Citation abbreviations (.json)</label>
+ <input type="file" ref="abbrevInput" class="hidden" accept=".json" @change="handleAbbrevFile($event)" />
+ <button class="upload-btn" @click="$refs.abbrevInput.click()">Upload Abbreviations</button>
+ <div v-if="abbrevFile" class="resource-item">&#128196; <a href="#" @click.prevent="downloadFile(abbrevFile, abbrevFile.name)">{{ abbrevFile.name }}</a> <span class="file-remove" @click="abbrevFile = null">&#10005;</span></div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Math -->
+ <div class="tab-panel" v-show="activeTab === 'math'">
+ <div class="form-group">
+ <label for="opt-math">Math rendering method</label>
+ <select id="opt-math" v-model="opts.mathMethod">
+ <option value="">Default (plain)</option>
+ <option value="mathjax">MathJax</option>
+ <option value="katex">KaTeX</option>
+ <option value="mathml">MathML</option>
+ <option value="webtex">WebTeX (images)</option>
+ <option value="gladtex">GladTeX</option>
+ </select>
+ </div>
+ </div>
+
+ <!-- Code -->
+ <div class="tab-panel" v-show="activeTab === 'code'">
+ <div class="options-grid">
+ <div class="form-group">
+ <label for="opt-highlight-style">Syntax highlight style</label>
+ <select id="opt-highlight-style" v-model="opts.highlightStyle">
+ <option value="">Default</option>
+ <option value="idiomatic">Idiomatic</option>
+ <option value="none">None</option>
+ <option value="pygments">Pygments</option>
+ <option value="kate">Kate</option>
+ <option value="monochrome">Monochrome</option>
+ <option value="breezedark">Breeze Dark</option>
+ <option value="espresso">Espresso</option>
+ <option value="zenburn">Zenburn</option>
+ <option value="haddock">Haddock</option>
+ <option value="tango">Tango</option>
+ <option value="custom">Custom...</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="opt-indented-code-classes">Indented code classes</label>
+ <input type="text" id="opt-indented-code-classes" v-model="opts.indentedCodeClasses" placeholder="e.g., haskell, python" />
+ </div>
+ </div>
+ <div class="resource-upload" v-show="opts.highlightStyle === 'custom'" style="margin-top: 0.75rem;">
+ <label>Custom highlight theme (.theme)</label>
+ <input type="file" ref="highlightThemeInput" class="hidden" accept=".theme" @change="handleHighlightTheme($event)" />
+ <button class="upload-btn" @click="$refs.highlightThemeInput.click()">Upload Theme</button>
+ <div v-if="highlightThemeFile" class="resource-item">&#127912; <a href="#" @click.prevent="downloadFile(highlightThemeFile, highlightThemeFile.name)">{{ highlightThemeFile.name }}</a> <span class="file-remove" @click="highlightThemeFile = null">&#10005;</span></div>
+ </div>
+ <div class="resource-upload" style="margin-top: 0.75rem;">
+ <label>Custom syntax definitions (.xml)</label>
+ <input type="file" ref="syntaxDefInput" class="hidden" accept=".xml" multiple @change="handleSyntaxDefs($event)" />
+ <button class="upload-btn" @click="$refs.syntaxDefInput.click()">Add Syntax Definitions</button>
+ <div class="file-list file-list-small" v-if="syntaxDefinitions.length > 0">
+ <div class="file-item" v-for="file in syntaxDefinitions" :key="file.name">
+ <span class="file-icon">&#128196;</span>
+ <a class="file-name" href="#" @click.prevent="downloadFile(file, file.name)">{{ file.name }}</a>
+ <span class="file-remove" @click="removeSyntaxDef(file.name)">&#10005;</span>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Format-specific Options -->
+ <div class="tab-panel" v-show="activeTab === 'format'">
+ <!-- HTML options -->
+ <div v-show="isHtmlFormat">
+ <div class="options-grid">
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-self-contained" v-model="opts.selfContained" />
+ <label for="opt-self-contained">Self-contained (embed resources)</label>
+ </div>
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-html-q-tags" v-model="opts.htmlQTags" />
+ <label for="opt-html-q-tags">Use &lt;q&gt; tags for quotes</label>
+ </div>
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-section-divs" v-model="opts.sectionDivs" />
+ <label for="opt-section-divs">Wrap sections in &lt;section&gt; tags</label>
+ </div>
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-ascii-html" v-model="opts.asciiHtml" />
+ <label for="opt-ascii-html">ASCII only</label>
+ </div>
+ </div>
+ <div class="options-grid" style="margin-top: 0.75rem;">
+ <div class="form-group">
+ <label for="opt-title-prefix">Title prefix</label>
+ <input type="text" id="opt-title-prefix" v-model="opts.titlePrefix" placeholder="Prefix" />
+ </div>
+ <div class="form-group">
+ <label for="opt-id-prefix">ID prefix</label>
+ <input type="text" id="opt-id-prefix" v-model="opts.idPrefix" placeholder="Prefix" />
+ </div>
+ <div class="form-group">
+ <label for="opt-email-obfuscation">Email obfuscation</label>
+ <select id="opt-email-obfuscation" v-model="opts.emailObfuscation">
+ <option value="">None</option>
+ <option value="javascript">JavaScript</option>
+ <option value="references">Character references</option>
+ </select>
+ </div>
+ </div>
+ <div class="resource-upload" style="margin-top: 0.75rem;">
+ <label>Custom CSS</label>
+ <input type="file" ref="cssInput" class="hidden" accept=".css" multiple @change="handleCssFiles($event)" />
+ <button class="upload-btn" @click="$refs.cssInput.click()">Add CSS Files</button>
+ <div class="file-list file-list-small" v-if="cssFiles.length > 0">
+ <div class="file-item" v-for="file in cssFiles" :key="file.name">
+ <span class="file-icon">&#127912;</span>
+ <a class="file-name" href="#" @click.prevent="downloadFile(file, file.name)">{{ file.name }}</a>
+ <span class="file-remove" @click="removeCssFile(file.name)">&#10005;</span>
+ </div>
+ </div>
+ </div>
+ </div>
+ <!-- Markdown options -->
+ <div v-show="isMarkdownFormat">
+ <div class="options-grid">
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-reference-links" v-model="opts.referenceLinks" />
+ <label for="opt-reference-links">Use reference-style links</label>
+ </div>
+ <div class="form-group">
+ <label for="opt-reference-location">Reference location</label>
+ <select id="opt-reference-location" v-model="opts.referenceLocation">
+ <option value="">Default (document)</option>
+ <option value="block">Block</option>
+ <option value="section">Section</option>
+ <option value="document">Document</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="opt-markdown-headings">Heading style</label>
+ <select id="opt-markdown-headings" v-model="opts.markdownHeadings">
+ <option value="">Default (ATX)</option>
+ <option value="atx">ATX (#)</option>
+ <option value="setext">Setext (underlined)</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <!-- RST options -->
+ <div v-show="effectiveOutputFormat === 'rst'">
+ <div class="options-grid">
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-reference-links-rst" v-model="opts.referenceLinksRst" />
+ <label for="opt-reference-links-rst">Use reference-style links</label>
+ </div>
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-list-tables" v-model="opts.listTables" />
+ <label for="opt-list-tables">Use list tables</label>
+ </div>
+ </div>
+ </div>
+ <!-- LaTeX options -->
+ <div v-show="supportsListOf">
+ <div class="options-grid">
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-lof" v-model="opts.lof" />
+ <label for="opt-lof">List of figures</label>
+ </div>
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-lot" v-model="opts.lot" />
+ <label for="opt-lot">List of tables</label>
+ </div>
+ </div>
+ </div>
+ <!-- Typst PDF note -->
+ <div v-show="effectiveOutputFormat === 'pdf-typst'">
+ <p style="color: var(--gray-500); font-size: 0.85rem; margin: 0;">
+ Output will be generated as Typst markup, then compiled to PDF.
+ </p>
+ </div>
+ <!-- Caption position -->
+ <div v-show="supportsCaptionPosition" style="margin-top: 0.75rem;">
+ <div class="options-grid">
+ <div class="form-group">
+ <label for="opt-figure-caption-position">Figure caption position</label>
+ <select id="opt-figure-caption-position" v-model="opts.figureCaptionPosition">
+ <option value="">Default</option>
+ <option value="above">Above</option>
+ <option value="below">Below</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="opt-table-caption-position">Table caption position</label>
+ <select id="opt-table-caption-position" v-model="opts.tableCaptionPosition">
+ <option value="">Default</option>
+ <option value="above">Above</option>
+ <option value="below">Below</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <!-- Top-level division -->
+ <div v-show="supportsTopLevelDivision" style="margin-top: 0.75rem;">
+ <div class="options-grid">
+ <div class="form-group">
+ <label for="opt-top-level-division">Top-level division</label>
+ <select id="opt-top-level-division" v-model="opts.topLevelDivision">
+ <option value="">Default</option>
+ <option value="section">Section</option>
+ <option value="chapter">Chapter</option>
+ <option value="part">Part</option>
+ </select>
+ </div>
+ </div>
+ </div>
+ <!-- ASCII (non-HTML) -->
+ <div v-show="supportsAscii && !isHtmlFormat" style="margin-top: 0.75rem;">
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-ascii" v-model="opts.ascii" />
+ <label for="opt-ascii">ASCII output only (escape non-ASCII characters)</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- EPUB -->
+ <div class="tab-panel" v-show="activeTab === 'epub'">
+ <div class="options-grid">
+ <div class="resource-upload">
+ <label>Cover image</label>
+ <input type="file" ref="epubCoverInput" class="hidden" accept="image/*" @change="handleEpubCover($event)" />
+ <button class="upload-btn" @click="$refs.epubCoverInput.click()">Upload Cover Image</button>
+ <div v-if="epubCoverImage" class="resource-item">&#128444; <a href="#" @click.prevent="downloadFile(epubCoverImage, epubCoverImage.name)">{{ epubCoverImage.name }}</a> <span class="file-remove" @click="epubCoverImage = null">&#10005;</span></div>
+ </div>
+ <div class="resource-upload">
+ <label>Embed fonts</label>
+ <input type="file" ref="epubFontInput" class="hidden" accept=".ttf,.otf,.woff,.woff2" multiple @change="handleEpubFonts($event)" />
+ <button class="upload-btn" @click="$refs.epubFontInput.click()">Upload Fonts</button>
+ <div class="file-list file-list-small" v-if="epubFonts.length > 0">
+ <div class="file-item" v-for="file in epubFonts" :key="file.name">
+ <span class="file-icon">&#128288;</span>
+ <a class="file-name" href="#" @click.prevent="downloadFile(file, file.name)">{{ file.name }}</a>
+ <span class="file-remove" @click="removeEpubFont(file.name)">&#10005;</span>
+ </div>
+ </div>
+ </div>
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-epub-title-page" v-model="opts.epubTitlePage" />
+ <label for="opt-epub-title-page">Include title page</label>
+ </div>
+ <div class="form-group">
+ <label for="opt-epub-subdirectory">EPUB subdirectory</label>
+ <input type="text" id="opt-epub-subdirectory" v-model="opts.epubSubdirectory" placeholder="EPUB (default)" />
+ </div>
+ </div>
+ </div>
+
+ <!-- Chunked HTML -->
+ <div class="tab-panel" v-show="activeTab === 'chunked'">
+ <div class="options-grid">
+ <div class="form-group">
+ <label for="opt-split-level">Split level (heading depth)</label>
+ <select id="opt-split-level" v-model="opts.splitLevel">
+ <option value="">Default (1)</option>
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="3">3</option>
+ <option value="4">4</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="opt-chunk-template">Chunk filename template</label>
+ <input type="text" id="opt-chunk-template" v-model="opts.chunkTemplate" placeholder="%s-%i.html" />
+ </div>
+ </div>
+ </div>
+
+ <!-- Notebook -->
+ <div class="tab-panel" v-show="activeTab === 'ipynb'">
+ <div class="options-grid">
+ <div class="form-group">
+ <label for="opt-ipynb-output">Output cell handling</label>
+ <select id="opt-ipynb-output" v-model="opts.ipynbOutput">
+ <option value="">Default (best)</option>
+ <option value="all">All outputs</option>
+ <option value="none">No outputs</option>
+ <option value="best">Best output only</option>
+ </select>
+ </div>
+ </div>
+ </div>
+
+ <!-- Template -->
+ <div class="tab-panel" v-show="activeTab === 'template'">
+ <div class="resource-upload">
+ <label>Custom template</label>
+ <input type="file" ref="templateInput" class="hidden" @change="handleTemplateFile($event)" />
+ <button class="upload-btn" @click="$refs.templateInput.click()">Upload Custom Template</button>
+ <div v-if="templateFile" class="resource-item">&#128196; <a href="#" @click.prevent="downloadFile(templateFile, templateFile.name)">{{ templateFile.name }}</a> <span class="file-remove" @click="templateFile = null">&#10005;</span></div>
+ </div>
+ <div class="resource-upload" v-show="showReferenceDoc">
+ <label>Reference document (for styles in docx/odt/pptx)</label>
+ <input type="file" ref="referenceDocInput" class="hidden" accept=".docx,.odt,.pptx" @change="handleReferenceDoc($event)" />
+ <button class="upload-btn" @click="$refs.referenceDocInput.click()">Upload Reference Document</button>
+ <div v-if="referenceDoc" class="resource-item">&#128196; <a href="#" @click.prevent="downloadFile(referenceDoc, referenceDoc.name)">{{ referenceDoc.name }}</a> <span class="file-remove" @click="referenceDoc = null">&#10005;</span></div>
+ </div>
+ <div class="resource-upload">
+ <label>Include in header (custom CSS, scripts, LaTeX packages)</label>
+ <input type="file" ref="headerInput" class="hidden" multiple @change="handleHeaderFiles($event)" />
+ <button class="upload-btn" @click="$refs.headerInput.click()">Add Header Files</button>
+ <div class="file-list file-list-small" v-if="headerFiles.length > 0">
+ <div class="file-item" v-for="file in headerFiles" :key="file.name">
+ <span class="file-icon">&#128196;</span>
+ <a class="file-name" href="#" @click.prevent="downloadFile(file, file.name)">{{ file.name }}</a>
+ <span class="file-remove" @click="removeHeaderFile(file.name)">&#10005;</span>
+ </div>
+ </div>
+ </div>
+ <div class="resource-upload">
+ <label>Include before body</label>
+ <input type="file" ref="beforeBodyInput" class="hidden" multiple @change="handleBeforeBodyFiles($event)" />
+ <button class="upload-btn" @click="$refs.beforeBodyInput.click()">Add Before-Body Files</button>
+ <div class="file-list file-list-small" v-if="beforeBodyFiles.length > 0">
+ <div class="file-item" v-for="file in beforeBodyFiles" :key="file.name">
+ <span class="file-icon">&#128196;</span>
+ <a class="file-name" href="#" @click.prevent="downloadFile(file, file.name)">{{ file.name }}</a>
+ <span class="file-remove" @click="removeBeforeBodyFile(file.name)">&#10005;</span>
+ </div>
+ </div>
+ </div>
+ <div class="resource-upload">
+ <label>Include after body</label>
+ <input type="file" ref="afterBodyInput" class="hidden" multiple @change="handleAfterBodyFiles($event)" />
+ <button class="upload-btn" @click="$refs.afterBodyInput.click()">Add After-Body Files</button>
+ <div class="file-list file-list-small" v-if="afterBodyFiles.length > 0">
+ <div class="file-item" v-for="file in afterBodyFiles" :key="file.name">
+ <span class="file-icon">&#128196;</span>
+ <a class="file-name" href="#" @click.prevent="downloadFile(file, file.name)">{{ file.name }}</a>
+ <span class="file-remove" @click="removeAfterBodyFile(file.name)">&#10005;</span>
+ </div>
+ </div>
+ </div>
+ <div class="variables-section">
+ <label>Variables</label>
+ <div class="custom-metadata">
+ <div class="custom-meta-row" v-for="(v, idx) in customVariables" :key="idx">
+ <input type="text" name="key" v-model="v.key" placeholder="variable name" />
+ <input type="text" name="value" v-model="v.value" placeholder="value" />
+ <button type="button" class="remove-meta" @click="customVariables.splice(idx, 1)">&#10005;</button>
+ </div>
+ </div>
+ <button type="button" class="add-metadata-btn" @click="customVariables.push({ key: '', value: '' })">+ Add variable</button>
+ </div>
+ </div>
+
+ <!-- Slides -->
+ <div class="tab-panel" v-show="activeTab === 'slides'">
+ <div class="options-grid">
+ <div class="form-group">
+ <label for="opt-slide-level">Slide level (heading depth)</label>
+ <select id="opt-slide-level" v-model="opts.slideLevel">
+ <option value="">Auto</option>
+ <option value="1">1</option>
+ <option value="2">2</option>
+ <option value="3">3</option>
+ </select>
+ </div>
+ <div class="checkbox-group">
+ <input type="checkbox" id="opt-incremental" v-model="opts.incremental" />
+ <label for="opt-incremental">Incremental lists</label>
+ </div>
+ </div>
+ </div>
+
+ <!-- Metadata -->
+ <div class="tab-panel" v-show="activeTab === 'metadata'">
+ <div class="resource-upload" style="margin-bottom: 0.75rem;">
+ <label>Metadata file (.yaml, .json)</label>
+ <input type="file" ref="metadataFileInput" class="hidden" accept=".yaml,.yml,.json" @change="handleMetadataFile($event)" />
+ <button class="upload-btn" @click="$refs.metadataFileInput.click()">Upload Metadata File</button>
+ <div v-if="metadataFile" class="resource-item">&#128196; <a href="#" @click.prevent="downloadFile(metadataFile, metadataFile.name)">{{ metadataFile.name }}</a> <span class="file-remove" @click="metadataFile = null">&#10005;</span></div>
+ </div>
+ <div class="options-grid">
+ <div class="form-group">
+ <label for="meta-title">title</label>
+ <input type="text" id="meta-title" v-model="metadata.title" placeholder="Document title" />
+ </div>
+ <div class="form-group">
+ <label for="meta-author">author</label>
+ <input type="text" id="meta-author" v-model="metadata.author" placeholder="Author name" />
+ </div>
+ <div class="form-group">
+ <label for="meta-date">date</label>
+ <input type="text" id="meta-date" v-model="metadata.date" placeholder="e.g., 2025-01-07" />
+ </div>
+ <div class="form-group">
+ <label for="meta-lang">lang</label>
+ <input type="text" id="meta-lang" v-model="metadata.lang" placeholder="e.g., en, de, fr" />
+ </div>
+ </div>
+ <div class="custom-metadata">
+ <div class="custom-meta-row" v-for="(m, idx) in customMetadata" :key="idx">
+ <input type="text" name="key" v-model="m.key" placeholder="field name" />
+ <input type="text" name="value" v-model="m.value" placeholder="value" />
+ <button type="button" class="remove-meta" @click="customMetadata.splice(idx, 1)">&#10005;</button>
+ </div>
+ </div>
+ <button type="button" class="add-metadata-btn" @click="customMetadata.push({ key: '', value: '' })">+ Add field</button>
+ </div>
+
+ <!-- Wrapping -->
+ <div class="tab-panel" v-show="activeTab === 'wrapping'">
+ <div class="options-grid">
+ <div class="form-group">
+ <label for="opt-wrap">Wrap mode</label>
+ <select id="opt-wrap" v-model="opts.wrap">
+ <option value="auto">Auto</option>
+ <option value="none">None</option>
+ <option value="preserve">Preserve</option>
+ </select>
+ </div>
+ <div class="form-group">
+ <label for="opt-columns">Line width (columns)</label>
+ <input type="text" id="opt-columns" v-model="opts.columns" placeholder="72" />
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Loading -->
+ <div class="loading" v-show="isConverting">
+ <div class="spinner"></div>
+ <p>Converting...</p>
+ </div>
+
+ <!-- Output Section -->
+ <div class="card" v-show="showOutput">
+ <div class="card-body">
+ <div class="output-actions">
+ <button class="download-btn" v-show="output" @click="download">&#128229; Download {{ outputFilenameActual }}</button>
+ <button class="copy-btn" v-show="output && !isBinaryOutput" @click="copyToClipboard">{{ copyBtnText }}</button>
+ </div>
+ <div class="messages">
+ <div v-for="(msg, idx) in messages" :key="idx" class="message" :class="msg.type">{{ msg.text }}</div>
+ </div>
+ <div class="output-preview" v-show="!isBinaryOutput && outputPreview">{{ outputPreview }}</div>
+ </div>
+ </div>
+ </div>
+
+ <!-- Loading Overlay -->
+ <div class="loading-overlay" v-show="!pandocReady">
+ <div class="loading-box">
+ <div class="spinner"></div>
+ <p>Loading pandoc.wasm...</p>
+ </div>
+ </div>
+ </div>
+
+ <script type="module">
+ import { pandoc, getExtensionsForFormat } from "./pandoc.js?sha1=XX";
+
+ // Make pandoc available globally for the app
+ window.pandocModule = { pandoc, getExtensionsForFormat };
+
+ // Lazy-load typst library only when needed
+ let typstLoaded = false;
+ let typstLoadingPromise = null;
+
+ window.loadTypst = async function() {
+ if (typstLoaded && typeof $typst !== 'undefined') return;
+ if (typstLoadingPromise) return typstLoadingPromise;
+
+ typstLoadingPromise = new Promise((resolve, reject) => {
+ const typstScript = document.createElement('script');
+ typstScript.type = 'module';
+ typstScript.src = 'https://cdn.jsdelivr.net/npm/@myriaddreamin/[email protected]/dist/esm/index.js';
+ typstScript.onload = () => {
+ const checkTypst = () => {
+ if (typeof $typst !== 'undefined') {
+ typstLoaded = true;
+ resolve();
+ } else {
+ setTimeout(checkTypst, 100);
+ }
+ };
+ checkTypst();
+ };
+ typstScript.onerror = () => reject(new Error('Failed to load Typst library'));
+ document.head.appendChild(typstScript);
+ });
+ return typstLoadingPromise;
+ };
+
+ // Petite Vue app definition
+ window.pandocApp = function() {
+ return {
+ // State
+ pandocReady: false,
+ selectedExample: '',
+ showExamplesBar: false,
+ inputMode: 'file',
+ files: {},
+ fileOrder: [],
+ auxFiles: {},
+ textInput: '',
+ inputFormat: 'auto',
+ outputFormat: 'auto',
+ outputFilename: '',
+ activeTab: 'general',
+ dragOver: false,
+ fileDraggingIdx: -1,
+ fileDragOverIdx: -1,
+
+ // Options
+ opts: {
+ standalone: true,
+ toc: false,
+ numberSections: false,
+ fileScope: false,
+ tocDepth: '3',
+ shiftHeading: '0',
+ trackChanges: '',
+ citationMethod: '',
+ mathMethod: '',
+ highlightStyle: '',
+ indentedCodeClasses: '',
+ selfContained: false,
+ htmlQTags: false,
+ sectionDivs: false,
+ asciiHtml: false,
+ titlePrefix: '',
+ idPrefix: '',
+ emailObfuscation: '',
+ referenceLinks: false,
+ referenceLocation: '',
+ markdownHeadings: '',
+ referenceLinksRst: false,
+ listTables: false,
+ lof: false,
+ lot: false,
+ figureCaptionPosition: '',
+ tableCaptionPosition: '',
+ topLevelDivision: '',
+ ascii: false,
+ epubTitlePage: true,
+ epubSubdirectory: '',
+ splitLevel: '',
+ chunkTemplate: '',
+ ipynbOutput: '',
+ slideLevel: '',
+ incremental: false,
+ wrap: 'auto',
+ columns: ''
+ },
+
+ // Metadata
+ metadata: { title: '', author: '', date: '', lang: '' },
+ customMetadata: [],
+ customVariables: [],
+
+ // Resource files
+ bibFile: null,
+ cslFile: null,
+ abbrevFile: null,
+ cssFiles: [],
+ highlightThemeFile: null,
+ syntaxDefinitions: [],
+ templateFile: null,
+ referenceDoc: null,
+ metadataFile: null,
+ epubCoverImage: null,
+ epubFonts: [],
+ headerFiles: [],
+ beforeBodyFiles: [],
+ afterBodyFiles: [],
+
+ // CSL
+ cslStyleName: '',
+ cslStylesList: [],
+ cslStylesLoaded: false,
+
+ // Extensions
+ inputExtensions: {},
+ outputExtensions: {},
+ inputExtensionsList: [],
+ outputExtensionsList: [],
+
+ // Output
+ isConverting: false,
+ showOutput: false,
+ output: null,
+ outputFilenameActual: '',
+ outputPreview: '',
+ messages: [],
+ copyBtnText: '📋 Copy to Clipboard',
+
+ // Format constants
+ formatByExtension: {
+ 'md': 'markdown', 'markdown': 'markdown', 'mkd': 'markdown',
+ 'html': 'html', 'htm': 'html',
+ 'tex': 'latex', 'latex': 'latex',
+ 'rst': 'rst', 'org': 'org', 'docx': 'docx', 'odt': 'odt',
+ 'epub': 'epub', 'txt': 'markdown', 'json': 'json', 'ipynb': 'ipynb',
+ 'xml': 'docbook', 'wiki': 'mediawiki', 'textile': 'textile',
+ 'rtf': 'rtf', 'bib': 'bibtex', 'csv': 'csv', 'tsv': 'tsv',
+ 'typ': 'typst', 'typst': 'typst', 'pptx': 'pptx'
+ },
+
+ extensionByFormat: {
+ 'html': 'html', 'html5': 'html', 'html4': 'html', 'chunkedhtml': 'zip',
+ 'markdown': 'md', 'markdown_strict': 'md', 'markdown_mmd': 'md', 'markdown_phpextra': 'md',
+ 'gfm': 'md', 'commonmark': 'md', 'commonmark_x': 'md',
+ 'latex': 'tex', 'beamer': 'tex', 'context': 'tex',
+ 'pdf': 'pdf', 'pdf-typst': 'pdf', 'docx': 'docx', 'odt': 'odt',
+ 'epub': 'epub', 'epub2': 'epub', 'epub3': 'epub',
+ 'rst': 'rst', 'org': 'org', 'plain': 'txt',
+ 'json': 'json', 'native': 'native',
+ 'docbook': 'xml', 'docbook4': 'xml', 'docbook5': 'xml',
+ 'jats': 'xml', 'tei': 'xml', 'man': '1', 'rtf': 'rtf',
+ 'textile': 'textile', 'mediawiki': 'wiki',
+ 'asciidoc': 'adoc', 'asciidoctor': 'adoc', 'asciidoc_legacy': 'adoc',
+ 'revealjs': 'html', 'slidy': 'html', 'slideous': 'html', 'dzslides': 'html', 's5': 'html',
+ 'ipynb': 'ipynb', 'typst': 'typ', 'texinfo': 'texi', 'ms': 'ms', 'icml': 'icml',
+ 'opml': 'opml', 'bibtex': 'bib', 'biblatex': 'bib', 'csljson': 'json',
+ 'pptx': 'pptx', 'djot': 'dj', 'fb2': 'fb2', 'opendocument': 'xml', 'vimdoc': 'txt'
+ },
+
+ slideFormats: ['revealjs', 'slidy', 'slideous', 'dzslides', 's5', 'beamer', 'pptx'],
+ htmlFormats: ['html', 'html4', 'html5', 'revealjs', 'slidy', 'slideous', 'dzslides', 's5', 'epub', 'epub2', 'epub3', 'chunkedhtml'],
+ docFormats: ['docx', 'odt', 'pptx'],
+ binaryFormats: ['docx', 'odt', 'pptx', 'epub', 'epub2', 'epub3', 'pdf', 'pdf-typst', 'chunkedhtml'],
+ markdownFormats: ['markdown', 'markdown_strict', 'markdown_mmd', 'markdown_phpextra', 'gfm', 'commonmark', 'commonmark_x'],
+ captionPositionFormats: ['html', 'html4', 'html5', 'latex', 'beamer', 'docx', 'odt', 'typst', 'pdf-typst'],
+ asciiFormats: ['html', 'html4', 'html5', 'markdown', 'markdown_strict', 'markdown_mmd', 'markdown_phpextra', 'gfm', 'commonmark', 'commonmark_x', 'docbook', 'docbook4', 'docbook5', 'jats', 'man', 'ms', 'latex', 'beamer'],
+ topLevelDivisionFormats: ['latex', 'beamer', 'context', 'docbook', 'docbook4', 'docbook5', 'tei'],
+ listOfFormats: ['latex', 'beamer', 'context'],
+ latexCitationFormats: ['latex', 'beamer', 'context'],
+
+ // Computed-like getters
+ get canConvert() {
+ const hasInput = this.inputMode === 'file' ? this.fileOrder.length > 0 : this.textInput.trim().length > 0;
+ return hasInput && this.pandocReady;
+ },
+
+ get effectiveOutputFormat() {
+ if (this.outputFormat !== 'auto') return this.outputFormat;
+ const outFile = this.outputFilename.trim() || this.outputFilenamePlaceholder;
+ if (outFile && outFile !== '(preview)') {
+ const ext = outFile.split('.').pop().toLowerCase();
+ const formatByOutputExtension = {
+ 'html': 'html', 'htm': 'html', 'md': 'markdown', 'markdown': 'markdown',
+ 'tex': 'latex', 'pdf': 'pdf-typst', 'docx': 'docx', 'odt': 'odt',
+ 'epub': 'epub', 'rst': 'rst', 'org': 'org', 'txt': 'plain',
+ 'json': 'json', 'native': 'native', 'xml': 'docbook', 'rtf': 'rtf',
+ 'adoc': 'asciidoc', 'ipynb': 'ipynb', 'typ': 'typst', 'pptx': 'pptx',
+ 'dj': 'djot', 'man': 'man', '1': 'man', 'ms': 'ms', 'texi': 'texinfo',
+ 'icml': 'icml', 'opml': 'opml', 'bib': 'bibtex', 'wiki': 'mediawiki'
+ };
+ return formatByOutputExtension[ext] || 'html';
+ }
+ return 'html';
+ },
+
+ get outputFilenamePlaceholder() {
+ if (this.outputFormat !== 'auto') {
+ const outExt = this.extensionByFormat[this.outputFormat] || this.outputFormat;
+ if (this.fileOrder.length > 0) {
+ const baseName = this.fileOrder[0].replace(/\.[^.]+$/, '');
+ return `${baseName}.${outExt}`;
+ }
+ return `output.${outExt}`;
+ }
+ return '(preview)';
+ },
+
+ get isHtmlFormat() { return this.htmlFormats.includes(this.effectiveOutputFormat); },
+ get isMarkdownFormat() { return this.markdownFormats.includes(this.effectiveOutputFormat); },
+ get isLatexLikeFormat() { return this.latexCitationFormats.includes(this.effectiveOutputFormat); },
+ get supportsCaptionPosition() { return this.captionPositionFormats.includes(this.effectiveOutputFormat); },
+ get supportsAscii() { return this.asciiFormats.includes(this.effectiveOutputFormat); },
+ get supportsTopLevelDivision() { return this.topLevelDivisionFormats.includes(this.effectiveOutputFormat); },
+ get supportsListOf() { return this.listOfFormats.includes(this.effectiveOutputFormat); },
+ get isBinaryOutput() { return this.binaryFormats.includes(this.effectiveOutputFormat); },
+
+ get showTrackChangesTab() {
+ let inFmt = this.inputFormat;
+ if (inFmt === 'auto' && this.fileOrder.length > 0) {
+ const ext = this.fileOrder[0].split('.').pop().toLowerCase();
+ inFmt = this.formatByExtension[ext] || '';
+ }
+ return inFmt === 'docx';
+ },
+
+ get showExtensionsTab() {
+ return (this.inputFormat !== 'auto' && this.inputExtensionsList.length > 0) ||
+ (this.outputFormat !== 'auto' && this.outputExtensionsList.length > 0);
+ },
+
+ get showFormatTab() {
+ const fmt = this.effectiveOutputFormat;
+ return this.isHtmlFormat || this.isMarkdownFormat || fmt === 'rst' ||
+ ['latex', 'beamer'].includes(fmt) || ['typst', 'pdf-typst'].includes(fmt) ||
+ this.supportsCaptionPosition || this.supportsAscii || this.supportsTopLevelDivision ||
+ this.supportsListOf || this.docFormats.includes(fmt);
+ },
+
+ get formatTabName() {
+ const fmt = this.effectiveOutputFormat;
+ if (this.isHtmlFormat) return 'HTML';
+ if (this.isMarkdownFormat) return 'Markdown';
+ if (fmt === 'rst') return 'RST';
+ if (fmt === 'latex') return 'LaTeX';
+ if (fmt === 'beamer') return 'Beamer';
+ if (['typst', 'pdf-typst'].includes(fmt)) return 'Typst';
+ if (fmt === 'context') return 'ConTeXt';
+ if (fmt === 'tei') return 'TEI';
+ if (fmt.startsWith('docbook')) return 'DocBook';
+ if (this.docFormats.includes(fmt)) return fmt.toUpperCase();
+ return fmt;
+ },
+
+ get showEpubTab() { return ['epub', 'epub2', 'epub3'].includes(this.effectiveOutputFormat); },
+ get showChunkedTab() { return this.effectiveOutputFormat === 'chunkedhtml'; },
+ get showIpynbTab() { return this.effectiveOutputFormat === 'ipynb'; },
+ get showSlidesTab() { return this.slideFormats.includes(this.effectiveOutputFormat); },
+ get showReferenceDoc() { return this.docFormats.includes(this.effectiveOutputFormat); },
+
+ // Methods
+ init() {
+ this.pandocReady = true;
+ console.log('Pandoc converter ready (Petite Vue edition)');
+ },
+
+ async loadExample() {
+ const tarpath = this.selectedExample;
+ if (!tarpath) return;
+
+ try {
+ const response = await fetch(tarpath);
+ const arrayBuffer = await response.arrayBuffer();
+ await this.loadExampleFromBuffer(arrayBuffer, tarpath.split('/').pop());
+ } catch (err) {
+ this.messages.push({ type: 'error', text: `Failed to load example: ${err.message}` });
+ }
+ },
+
+ async uploadExample(e) {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ try {
+ const arrayBuffer = await file.arrayBuffer();
+ e.target.value = '';
+ await this.loadExampleFromBuffer(arrayBuffer, file.name);
+ } catch (err) {
+ this.showOutput = true;
+ this.messages.push({ type: 'error', text: `Failed to load example: ${err.message}` });
+ }
+ },
+
+ async loadExampleFromBuffer(arrayBuffer, filename) {
+ const reader = await tarjs.TarReader.load(new Blob([arrayBuffer]));
+
+ // Clear existing state
+ this.files = {};
+ this.fileOrder = [];
+ this.auxFiles = {};
+ this.textInput = '';
+ this.templateFile = null;
+ this.referenceDoc = null;
+ this.bibFile = null;
+ this.cslFile = null;
+ this.cslStyleName = '';
+ this.cssFiles = [];
+ this.headerFiles = [];
+ this.beforeBodyFiles = [];
+ this.afterBodyFiles = [];
+ this.highlightThemeFile = null;
+ this.syntaxDefinitions = [];
+ this.abbrevFile = null;
+ this.metadataFile = null;
+ this.epubCoverImage = null;
+ this.epubFonts = [];
+
+ let optionsJson = null;
+ let stdinContent = null;
+ const extractedFiles = {};
+
+ // First pass: extract all files
+ for (const fileInfo of reader.fileInfos) {
+ const name = fileInfo.name;
+ if (name.endsWith('/') || fileInfo.size === 0) continue;
+ const fname = name.split('/').pop();
+
+ if (fname === 'options.json') {
+ optionsJson = JSON.parse(await reader.getTextFile(name));
+ } else if (fname === 'stdin') {
+ stdinContent = await reader.getTextFile(name);
+ } else {
+ const blob = await reader.getFileBlob(name);
+ extractedFiles[fname] = new File([blob], fname);
+ }
+ }
+
+ // Helper to get file and remove from extractedFiles
+ const takeFile = (name) => {
+ if (name && extractedFiles[name]) {
+ const f = extractedFiles[name];
+ delete extractedFiles[name];
+ return f;
+ }
+ return null;
+ };
+
+ // Helper for arrays of filenames
+ const takeFiles = (names) => {
+ if (!names) return [];
+ const arr = Array.isArray(names) ? names : [names];
+ return arr.map(takeFile).filter(f => f);
+ };
+
+ // Route files based on options.json
+ if (optionsJson) {
+ // Template
+ this.templateFile = takeFile(optionsJson.template);
+
+ // Reference doc
+ this.referenceDoc = takeFile(optionsJson['reference-doc']);
+
+ // Bibliography
+ const bibFiles = takeFiles(optionsJson.bibliography);
+ if (bibFiles.length > 0) this.bibFile = bibFiles[0];
+
+ // CSL style
+ this.cslFile = takeFile(optionsJson.csl);
+
+ // CSS files
+ this.cssFiles = takeFiles(optionsJson.css);
+
+ // Include files
+ this.headerFiles = takeFiles(optionsJson['include-in-header']);
+ this.beforeBodyFiles = takeFiles(optionsJson['include-before-body']);
+ this.afterBodyFiles = takeFiles(optionsJson['include-after-body']);
+
+ // Highlight theme (if it's a file)
+ const hlStyle = optionsJson['highlight-style'];
+ if (hlStyle && extractedFiles[hlStyle]) {
+ this.highlightThemeFile = takeFile(hlStyle);
+ this.opts.highlightStyle = 'custom';
+ } else if (hlStyle) {
+ this.opts.highlightStyle = hlStyle;
+ }
+
+ // Syntax definitions
+ this.syntaxDefinitions = takeFiles(optionsJson['syntax-definition']);
+
+ // Abbreviations
+ this.abbrevFile = takeFile(optionsJson.abbreviations);
+
+ // Metadata file
+ this.metadataFile = takeFile(optionsJson['metadata-file']);
+
+ // EPUB cover image
+ this.epubCoverImage = takeFile(optionsJson['epub-cover-image']);
+
+ // EPUB fonts
+ this.epubFonts = takeFiles(optionsJson['epub-fonts']);
+
+ // Input files go to main files area
+ const inputFileNames = optionsJson['input-files'];
+ if (inputFileNames) {
+ const inputArr = Array.isArray(inputFileNames) ? inputFileNames : [inputFileNames];
+ for (const name of inputArr) {
+ const f = takeFile(name);
+ if (f) {
+ this.files[name] = f;
+ this.fileOrder.push(name);
+ }
+ }
+ }
+
+ // Set input format
+ if (optionsJson.from) {
+ this.inputFormat = optionsJson.from.split(/[+-]/)[0];
+ }
+ // Set output format
+ if (optionsJson.to) {
+ this.outputFormat = optionsJson.to.split(/[+-]/)[0];
+ }
+ // Set standalone
+ if (optionsJson.standalone !== undefined) {
+ this.opts.standalone = optionsJson.standalone;
+ }
+ // Other options
+ if (optionsJson['table-of-contents']) this.opts.toc = true;
+ if (optionsJson['number-sections']) this.opts.numberSections = true;
+ if (optionsJson.citeproc) this.opts.citationMethod = 'citeproc';
+ if (optionsJson['embed-resources']) this.opts.selfContained = true;
+ if (optionsJson['mathml']) this.opts.mathMethod = 'mathml';
+ }
+
+ // Remaining files go to auxiliary files
+ for (const [name, file] of Object.entries(extractedFiles)) {
+ this.auxFiles[name] = file;
+ }
+
+ // Handle stdin content
+ if (stdinContent) {
+ this.inputMode = 'text';
+ this.textInput = stdinContent;
+ } else if (this.fileOrder.length > 0) {
+ this.inputMode = 'file';
+ }
+
+ // Auto-convert after loading example
+ await this.convert();
+
+ // Update extensions UI after conversion completes
+ this.onInputFormatChange();
+ this.onOutputFormatChange();
+ },
+
+ async downloadAsExample() {
+ const writer = new tarjs.TarWriter();
+
+ // Build options for the example
+ const opts = {};
+ if (this.inputFormat !== 'auto') opts.from = this.inputFormat;
+ if (this.outputFormat !== 'auto') opts.to = this.outputFormat;
+ if (this.opts.standalone) opts.standalone = true;
+ if (this.opts.toc) opts['table-of-contents'] = true;
+ if (this.opts.numberSections) opts['number-sections'] = true;
+ if (this.opts.fileScope) opts['file-scope'] = true;
+ if (this.opts.citationMethod === 'citeproc') opts.citeproc = true;
+ if (this.opts.mathMethod === 'mathml') opts.mathml = true;
+ if (this.opts.selfContained) opts['embed-resources'] = true;
+ if (this.opts.highlightStyle && this.opts.highlightStyle !== 'custom') {
+ opts['highlight-style'] = this.opts.highlightStyle;
+ }
+
+ // Helper to add a file to the tar
+ const addFile = async (file, name) => {
+ writer.addFile(name, file);
+ };
+
+ // Input files or stdin
+ if (this.inputMode === 'text' && this.textInput.trim()) {
+ writer.addFile('stdin', this.textInput);
+ } else if (this.inputMode === 'file' && this.fileOrder.length > 0) {
+ opts['input-files'] = this.fileOrder;
+ for (const name of this.fileOrder) {
+ if (this.files[name]) await addFile(this.files[name], name);
+ }
+ }
+
+ // Auxiliary files
+ for (const [name, file] of Object.entries(this.auxFiles)) {
+ await addFile(file, name);
+ }
+
+ // Resource files
+ if (this.templateFile) {
+ opts.template = this.templateFile.name;
+ await addFile(this.templateFile, this.templateFile.name);
+ }
+ if (this.referenceDoc) {
+ opts['reference-doc'] = this.referenceDoc.name;
+ await addFile(this.referenceDoc, this.referenceDoc.name);
+ }
+ if (this.bibFile) {
+ opts.bibliography = this.bibFile.name;
+ await addFile(this.bibFile, this.bibFile.name);
+ }
+ if (this.cslFile) {
+ opts.csl = this.cslFile.name;
+ await addFile(this.cslFile, this.cslFile.name);
+ }
+ if (this.abbrevFile) {
+ opts.abbreviations = this.abbrevFile.name;
+ await addFile(this.abbrevFile, this.abbrevFile.name);
+ }
+ if (this.metadataFile) {
+ opts['metadata-file'] = this.metadataFile.name;
+ await addFile(this.metadataFile, this.metadataFile.name);
+ }
+ if (this.highlightThemeFile) {
+ opts['highlight-style'] = this.highlightThemeFile.name;
+ await addFile(this.highlightThemeFile, this.highlightThemeFile.name);
+ }
+ if (this.cssFiles.length > 0) {
+ opts.css = this.cssFiles.map(f => f.name);
+ for (const file of this.cssFiles) await addFile(file, file.name);
+ }
+ if (this.syntaxDefinitions.length > 0) {
+ opts['syntax-definition'] = this.syntaxDefinitions.map(f => f.name);
+ for (const file of this.syntaxDefinitions) await addFile(file, file.name);
+ }
+ if (this.headerFiles.length > 0) {
+ opts['include-in-header'] = this.headerFiles.map(f => f.name);
+ for (const file of this.headerFiles) await addFile(file, file.name);
+ }
+ if (this.beforeBodyFiles.length > 0) {
+ opts['include-before-body'] = this.beforeBodyFiles.map(f => f.name);
+ for (const file of this.beforeBodyFiles) await addFile(file, file.name);
+ }
+ if (this.afterBodyFiles.length > 0) {
+ opts['include-after-body'] = this.afterBodyFiles.map(f => f.name);
+ for (const file of this.afterBodyFiles) await addFile(file, file.name);
+ }
+ if (this.epubCoverImage) {
+ opts['epub-cover-image'] = this.epubCoverImage.name;
+ await addFile(this.epubCoverImage, this.epubCoverImage.name);
+ }
+ if (this.epubFonts.length > 0) {
+ opts['epub-fonts'] = this.epubFonts.map(f => f.name);
+ for (const file of this.epubFonts) await addFile(file, file.name);
+ }
+
+ // Add options.json
+ writer.addFile('options.json', JSON.stringify(opts, null, 2));
+
+ // Create tar and download
+ const blob = await writer.write();
+ const url = URL.createObjectURL(blob);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = 'example.tar';
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ },
+
+ formatFileSize(bytes) {
+ if (bytes < 1024) return bytes + ' B';
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
+ return (bytes / (1024 * 1024)).toFixed(1) + ' MB';
+ },
+
+ downloadFile(file, name) {
+ const url = URL.createObjectURL(file);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = name;
+ document.body.appendChild(a);
+ a.click();
+ document.body.removeChild(a);
+ URL.revokeObjectURL(url);
+ },
+
+ handleDrop(e) {
+ this.dragOver = false;
+ this.handleFiles(Array.from(e.dataTransfer.files));
+ },
+
+ handleFileInput(e) {
+ this.handleFiles(Array.from(e.target.files));
+ e.target.value = '';
+ },
+
+ handleFiles(fileList) {
+ fileList.forEach(file => {
+ if (!this.files[file.name]) {
+ this.fileOrder.push(file.name);
+ }
+ this.files[file.name] = file;
+ });
+ this.updateExtensions();
+ },
+
+ removeFile(name) {
+ delete this.files[name];
+ this.fileOrder = this.fileOrder.filter(n => n !== name);
+ },
+
+ onFileDragStart(idx, e) {
+ this.fileDraggingIdx = idx;
+ e.dataTransfer.effectAllowed = 'move';
+ },
+
+ onFileDragOver(idx) {
+ this.fileDragOverIdx = idx;
+ },
+
+ onFileDrop(idx) {
+ this.fileDragOverIdx = -1;
+ if (this.fileDraggingIdx !== -1 && this.fileDraggingIdx !== idx) {
+ const draggedName = this.fileOrder[this.fileDraggingIdx];
+ this.fileOrder.splice(this.fileDraggingIdx, 1);
+ this.fileOrder.splice(idx, 0, draggedName);
+ }
+ },
+
+ handleAuxFiles(e) {
+ Array.from(e.target.files).forEach(file => { this.auxFiles[file.name] = file; });
+ e.target.value = '';
+ },
+
+ handleAuxDir(e) {
+ Array.from(e.target.files).forEach(file => {
+ const path = file.webkitRelativePath || file.name;
+ this.auxFiles[path] = file;
+ });
+ e.target.value = '';
+ },
+
+ removeAuxFile(name) { delete this.auxFiles[name]; },
+
+ // Resource file handlers
+ handleBibFile(e) { if (e.target.files[0]) this.bibFile = e.target.files[0]; },
+ handleCslFile(e) { if (e.target.files[0]) { this.cslFile = e.target.files[0]; this.cslStyleName = ''; } },
+ handleAbbrevFile(e) { if (e.target.files[0]) this.abbrevFile = e.target.files[0]; },
+ handleHighlightTheme(e) { if (e.target.files[0]) this.highlightThemeFile = e.target.files[0]; },
+ handleTemplateFile(e) { if (e.target.files[0]) this.templateFile = e.target.files[0]; },
+ handleReferenceDoc(e) { if (e.target.files[0]) this.referenceDoc = e.target.files[0]; },
+ handleMetadataFile(e) { if (e.target.files[0]) this.metadataFile = e.target.files[0]; },
+ handleEpubCover(e) { if (e.target.files[0]) this.epubCoverImage = e.target.files[0]; },
+
+ handleCssFiles(e) {
+ Array.from(e.target.files).forEach(file => {
+ if (!this.cssFiles.find(f => f.name === file.name)) this.cssFiles.push(file);
+ });
+ e.target.value = '';
+ },
+ removeCssFile(name) { this.cssFiles = this.cssFiles.filter(f => f.name !== name); },
+
+ handleSyntaxDefs(e) {
+ Array.from(e.target.files).forEach(file => {
+ if (!this.syntaxDefinitions.find(f => f.name === file.name)) this.syntaxDefinitions.push(file);
+ });
+ e.target.value = '';
+ },
+ removeSyntaxDef(name) { this.syntaxDefinitions = this.syntaxDefinitions.filter(f => f.name !== name); },
+
+ handleEpubFonts(e) {
+ Array.from(e.target.files).forEach(file => {
+ if (!this.epubFonts.find(f => f.name === file.name)) this.epubFonts.push(file);
+ });
+ e.target.value = '';
+ },
+ removeEpubFont(name) { this.epubFonts = this.epubFonts.filter(f => f.name !== name); },
+
+ handleHeaderFiles(e) {
+ Array.from(e.target.files).forEach(file => {
+ if (!this.headerFiles.find(f => f.name === file.name)) this.headerFiles.push(file);
+ });
+ e.target.value = '';
+ },
+ removeHeaderFile(name) { this.headerFiles = this.headerFiles.filter(f => f.name !== name); },
+
+ handleBeforeBodyFiles(e) {
+ Array.from(e.target.files).forEach(file => {
+ if (!this.beforeBodyFiles.find(f => f.name === file.name)) this.beforeBodyFiles.push(file);
+ });
+ e.target.value = '';
+ },
+ removeBeforeBodyFile(name) { this.beforeBodyFiles = this.beforeBodyFiles.filter(f => f.name !== name); },
+
+ handleAfterBodyFiles(e) {
+ Array.from(e.target.files).forEach(file => {
+ if (!this.afterBodyFiles.find(f => f.name === file.name)) this.afterBodyFiles.push(file);
+ });
+ e.target.value = '';
+ },
+ removeAfterBodyFile(name) { this.afterBodyFiles = this.afterBodyFiles.filter(f => f.name !== name); },
+
+ // CSL styles
+ async loadCslStyles() {
+ if (this.cslStylesLoaded) return;
+ try {
+ const response = await fetch('https://data.jsdelivr.com/v1/package/gh/citation-style-language/styles@master/flat');
+ if (!response.ok) throw new Error('Failed to fetch styles list');
+ const data = await response.json();
+ this.cslStylesList = data.files
+ .map(f => f.name)
+ .filter(name => name.endsWith('.csl') && name.lastIndexOf('/') === 0)
+ .map(name => name.slice(1, -4))
+ .sort();
+ this.cslStylesLoaded = true;
+ } catch (err) {
+ console.error('Failed to load CSL styles list:', err);
+ this.cslStylesList = ['apa', 'chicago-author-date', 'ieee', 'modern-language-association', 'vancouver', 'harvard-cite-them-right', 'nature', 'science'];
+ }
+ },
+
+ async fetchCslStyle() {
+ if (!this.cslStyleName.trim()) {
+ alert('Please enter or select a style name');
+ return;
+ }
+ try {
+ const url = `https://cdn.jsdelivr.net/gh/citation-style-language/styles@master/${this.cslStyleName}.csl`;
+ const response = await fetch(url);
+ if (!response.ok) throw new Error(`Style "${this.cslStyleName}" not found`);
+ const cslContent = await response.text();
+ const blob = new Blob([cslContent], { type: 'application/xml' });
+ this.cslFile = new File([blob], `${this.cslStyleName}.csl`, { type: 'application/xml' });
+ } catch (err) {
+ alert(`Failed to fetch style: ${err.message}`);
+ }
+ },
+
+ // Extensions
+ async updateExtensions() {
+ this.inputExtensions = {};
+ this.outputExtensions = {};
+ this.inputExtensionsList = [];
+ this.outputExtensionsList = [];
+
+ if (this.inputFormat !== 'auto') {
+ try {
+ const extData = await window.pandocModule.getExtensionsForFormat(this.inputFormat);
+ this.inputExtensionsList = Object.entries(extData)
+ .map(([name, defaultOn]) => ({ name, defaultOn }))
+ .sort((a, b) => a.name.localeCompare(b.name));
+ } catch (e) { console.warn('Could not get input extensions:', e); }
+ }
+
+ const outFmt = this.outputFormat === 'pdf-typst' ? 'typst' : this.outputFormat;
+ if (outFmt !== 'auto') {
+ try {
+ const extData = await window.pandocModule.getExtensionsForFormat(outFmt);
+ this.outputExtensionsList = Object.entries(extData)
+ .map(([name, defaultOn]) => ({ name, defaultOn }))
+ .sort((a, b) => a.name.localeCompare(b.name));
+ } catch (e) { console.warn('Could not get output extensions:', e); }
+ }
+ },
+
+ getExtensionChecked(direction, ext) {
+ const extState = direction === 'input' ? this.inputExtensions : this.outputExtensions;
+ if (extState[ext.name] !== undefined) return extState[ext.name];
+ return ext.defaultOn;
+ },
+
+ toggleExtension(direction, ext, e) {
+ const extState = direction === 'input' ? this.inputExtensions : this.outputExtensions;
+ if (e.target.checked) {
+ extState[ext.name] = true;
+ } else {
+ if (ext.defaultOn) {
+ extState[ext.name] = false;
+ } else {
+ delete extState[ext.name];
+ }
+ }
+ },
+
+ onInputFormatChange() { this.updateExtensions(); },
+ onOutputFormatChange() { this.updateExtensions(); },
+
+ buildFormatWithExtensions(baseFormat, extensions) {
+ if (!baseFormat || baseFormat === 'auto') return baseFormat;
+ let formatStr = baseFormat;
+ for (const [ext, enabled] of Object.entries(extensions)) {
+ if (enabled === true) formatStr += `+${ext}`;
+ else if (enabled === false) formatStr += `-${ext}`;
+ }
+ return formatStr;
+ },
+
+ buildOptions() {
+ const opts = {};
+
+ // Input format
+ if (this.inputFormat !== 'auto') {
+ opts.from = this.buildFormatWithExtensions(this.inputFormat, this.inputExtensions);
+ } else if (this.inputMode === 'file' && this.fileOrder.length > 0) {
+ const ext = this.fileOrder[0].split('.').pop().toLowerCase();
+ if (this.formatByExtension[ext]) {
+ opts.from = this.buildFormatWithExtensions(this.formatByExtension[ext], this.inputExtensions);
+ }
+ }
+
+ // Output format
+ if (this.outputFormat !== 'auto') {
+ opts.to = this.buildFormatWithExtensions(this.outputFormat, this.outputExtensions);
+ }
+
+ // Output file
+ let finalOutFile = this.outputFilename.trim() || this.outputFilenamePlaceholder;
+ if (!finalOutFile || finalOutFile === '(preview)') {
+ const effectiveFmt = this.effectiveOutputFormat || 'html';
+ const defaultExt = this.extensionByFormat[effectiveFmt] || 'html';
+ finalOutFile = `output.${defaultExt}`;
+ }
+ opts['output-file'] = finalOutFile;
+ this.outputFilenameActual = finalOutFile;
+
+ // Input files
+ if (this.inputMode === 'file') {
+ opts['input-files'] = this.fileOrder;
+ }
+
+ // General options
+ if (this.opts.standalone) opts.standalone = true;
+ if (this.opts.toc) {
+ opts['table-of-contents'] = true;
+ opts['toc-depth'] = parseInt(this.opts.tocDepth);
+ }
+ if (this.opts.numberSections) opts['number-sections'] = true;
+ if (this.opts.fileScope) opts['file-scope'] = true;
+ const shiftHeading = parseInt(this.opts.shiftHeading);
+ if (shiftHeading !== 0) opts['shift-heading-level-by'] = shiftHeading;
+
+ // Track changes
+ if (this.opts.trackChanges) opts['track-changes'] = this.opts.trackChanges;
+
+ // Citations
+ if (this.opts.citationMethod === 'citeproc') opts.citeproc = true;
+ else if (this.opts.citationMethod === 'natbib') opts.natbib = true;
+ else if (this.opts.citationMethod === 'biblatex') opts.biblatex = true;
+ if (this.bibFile) opts.bibliography = this.bibFile.name;
+ if (this.opts.citationMethod === 'citeproc') {
+ if (this.cslFile) opts.csl = this.cslFile.name;
+ if (this.abbrevFile) opts['citation-abbreviations'] = this.abbrevFile.name;
+ }
+
+ // Math
+ if (this.opts.mathMethod) opts['html-math-method'] = this.opts.mathMethod;
+
+ // Code highlighting
+ if (this.opts.highlightStyle === 'custom' && this.highlightThemeFile) {
+ opts['highlight-style'] = this.highlightThemeFile.name;
+ } else if (this.opts.highlightStyle && this.opts.highlightStyle !== 'custom') {
+ opts['highlight-style'] = this.opts.highlightStyle;
+ }
+ if (this.syntaxDefinitions.length > 0) {
+ opts['syntax-definition'] = this.syntaxDefinitions.map(f => f.name);
+ }
+ if (this.opts.indentedCodeClasses.trim()) {
+ opts['indented-code-classes'] = this.opts.indentedCodeClasses.trim();
+ }
+
+ const baseOutFmt = this.effectiveOutputFormat;
+
+ // HTML options
+ if (this.htmlFormats.includes(baseOutFmt)) {
+ if (this.opts.selfContained) opts['embed-resources'] = true;
+ if (this.opts.htmlQTags) opts['html-q-tags'] = true;
+ if (this.opts.sectionDivs) opts['section-divs'] = true;
+ if (this.opts.titlePrefix.trim()) opts['title-prefix'] = this.opts.titlePrefix.trim();
+ if (this.opts.emailObfuscation) opts['email-obfuscation'] = this.opts.emailObfuscation;
+ if (this.opts.idPrefix.trim()) opts['id-prefix'] = this.opts.idPrefix.trim();
+ if (this.cssFiles.length > 0) opts.css = this.cssFiles.map(f => f.name);
+ }
+
+ // Markdown options
+ if (this.markdownFormats.includes(baseOutFmt)) {
+ if (this.opts.referenceLinks) opts['reference-links'] = true;
+ if (this.opts.referenceLocation) opts['reference-location'] = this.opts.referenceLocation;
+ if (this.opts.markdownHeadings) opts['markdown-headings'] = this.opts.markdownHeadings;
+ }
+
+ // RST options
+ if (baseOutFmt === 'rst') {
+ if (this.opts.referenceLinksRst) opts['reference-links'] = true;
+ if (this.opts.listTables) opts['list-tables'] = true;
+ }
+
+ // Caption position
+ if (this.captionPositionFormats.includes(baseOutFmt)) {
+ if (this.opts.figureCaptionPosition) opts['figure-caption-position'] = this.opts.figureCaptionPosition;
+ if (this.opts.tableCaptionPosition) opts['table-caption-position'] = this.opts.tableCaptionPosition;
+ }
+
+ // ASCII
+ if (this.asciiFormats.includes(baseOutFmt)) {
+ const asciiChecked = this.htmlFormats.includes(baseOutFmt) ? this.opts.asciiHtml : this.opts.ascii;
+ if (asciiChecked) opts.ascii = true;
+ }
+
+ // List of figures/tables
+ if (this.listOfFormats.includes(baseOutFmt)) {
+ if (this.opts.lof) opts['list-of-figures'] = true;
+ if (this.opts.lot) opts['list-of-tables'] = true;
+ }
+
+ // Top-level division
+ if (this.topLevelDivisionFormats.includes(baseOutFmt) && this.opts.topLevelDivision) {
+ opts['top-level-division'] = this.opts.topLevelDivision;
+ }
+
+ // EPUB options
+ if (['epub', 'epub2', 'epub3'].includes(baseOutFmt)) {
+ if (this.epubCoverImage) opts['epub-cover-image'] = this.epubCoverImage.name;
+ if (this.epubFonts.length > 0) opts['epub-fonts'] = this.epubFonts.map(f => f.name);
+ if (!this.opts.epubTitlePage) opts['epub-title-page'] = false;
+ if (this.opts.epubSubdirectory.trim()) opts['epub-subdirectory'] = this.opts.epubSubdirectory.trim();
+ }
+
+ // Template
+ if (this.templateFile) opts.template = this.templateFile.name;
+
+ // Include files
+ if (this.headerFiles.length > 0) opts['include-in-header'] = this.headerFiles.map(f => f.name);
+ if (this.beforeBodyFiles.length > 0) opts['include-before-body'] = this.beforeBodyFiles.map(f => f.name);
+ if (this.afterBodyFiles.length > 0) opts['include-after-body'] = this.afterBodyFiles.map(f => f.name);
+
+ // Variables
+ const variables = {};
+ this.customVariables.forEach(v => {
+ if (v.key.trim() && v.value.trim()) variables[v.key.trim()] = v.value.trim();
+ });
+ if (Object.keys(variables).length > 0) opts.variables = variables;
+
+ // Chunked HTML
+ if (baseOutFmt === 'chunkedhtml') {
+ if (this.opts.splitLevel) opts['split-level'] = parseInt(this.opts.splitLevel);
+ if (this.opts.chunkTemplate.trim()) opts['chunk-template'] = this.opts.chunkTemplate.trim();
+ }
+
+ // Notebook
+ if (baseOutFmt === 'ipynb' && this.opts.ipynbOutput) {
+ opts['ipynb-output'] = this.opts.ipynbOutput;
+ }
+
+ // Reference doc
+ if (this.docFormats.includes(baseOutFmt) && this.referenceDoc) {
+ opts['reference-doc'] = this.referenceDoc.name;
+ }
+
+ // Slides
+ if (this.slideFormats.includes(baseOutFmt)) {
+ if (this.opts.slideLevel) opts['slide-level'] = parseInt(this.opts.slideLevel);
+ if (this.opts.incremental) opts.incremental = true;
+ }
+
+ // Metadata file
+ if (this.metadataFile) opts['metadata-file'] = this.metadataFile.name;
+
+ // Metadata
+ const metadata = {};
+ if (this.metadata.title.trim()) metadata.title = this.metadata.title.trim();
+ if (this.metadata.author.trim()) metadata.author = [this.metadata.author.trim()];
+ if (this.metadata.date.trim()) metadata.date = this.metadata.date.trim();
+ if (this.metadata.lang.trim()) metadata.lang = this.metadata.lang.trim();
+ this.customMetadata.forEach(m => {
+ if (m.key.trim() && m.value.trim()) metadata[m.key.trim()] = m.value.trim();
+ });
+ if (Object.keys(metadata).length > 0) opts.metadata = metadata;
+
+ // Wrapping
+ if (this.opts.wrap !== 'auto') opts.wrap = this.opts.wrap;
+ if (this.opts.columns.trim()) opts.columns = parseInt(this.opts.columns);
+
+ return opts;
+ },
+
+ async convert() {
+ this.isConverting = true;
+ this.showOutput = false;
+ this.messages = [];
+ this.output = null;
+ this.outputPreview = '';
+
+ try {
+ const options = this.buildOptions();
+
+ // Build files object
+ const files = { ...this.files, ...this.auxFiles };
+ if (this.bibFile) files[this.bibFile.name] = this.bibFile;
+ if (this.cslFile) files[this.cslFile.name] = this.cslFile;
+ if (this.abbrevFile) files[this.abbrevFile.name] = this.abbrevFile;
+ this.cssFiles.forEach(file => { files[file.name] = file; });
+ if (this.highlightThemeFile) files[this.highlightThemeFile.name] = this.highlightThemeFile;
+ this.syntaxDefinitions.forEach(file => { files[file.name] = file; });
+ if (this.templateFile) files[this.templateFile.name] = this.templateFile;
+ if (this.referenceDoc) files[this.referenceDoc.name] = this.referenceDoc;
+ if (this.metadataFile) files[this.metadataFile.name] = this.metadataFile;
+ if (this.epubCoverImage) files[this.epubCoverImage.name] = this.epubCoverImage;
+ this.epubFonts.forEach(font => { files[font.name] = font; });
+ this.headerFiles.forEach(file => { files[file.name] = file; });
+ this.beforeBodyFiles.forEach(file => { files[file.name] = file; });
+ this.afterBodyFiles.forEach(file => { files[file.name] = file; });
+
+ const stdin = this.inputMode === 'text' ? this.textInput : null;
+ const isPdfTypst = this.outputFormat === 'pdf-typst';
+
+ if (isPdfTypst) {
+ await window.loadTypst();
+ const typstOptions = { ...options };
+ typstOptions.to = this.buildFormatWithExtensions('typst', this.outputExtensions);
+ typstOptions.standalone = true;
+ delete typstOptions['output-file'];
+
+ const typstResult = await window.pandocModule.pandoc(typstOptions, stdin, files);
+ if (typstResult.stderr && typstResult.stderr.includes('ERROR')) {
+ throw new Error(typstResult.stderr);
+ }
+
+ const typstContent = typstResult.stdout;
+ let pdfData;
+ try {
+ $typst.resetShadow();
+ for (const [path, file] of Object.entries(files)) {
+ const arrayBuffer = await file.arrayBuffer();
+ const uint8Array = new Uint8Array(arrayBuffer);
+ $typst.mapShadow('/' + path, uint8Array);
+ if (!path.startsWith('/')) $typst.mapShadow(path, uint8Array);
+ }
+ const mainTypstPath = '/main.typ';
+ const typstBytes = new TextEncoder().encode(typstContent);
+ $typst.mapShadow(mainTypstPath, typstBytes);
+ pdfData = await $typst.pdf({ mainFilePath: mainTypstPath });
+ } catch (typstError) {
+ const errorStr = String(typstError);
+ const messageMatch = errorStr.match(/message:\s*"([^"]+)"/);
+ let errorMsg = messageMatch ? messageMatch[1] : errorStr;
+ throw new Error(`Typst: ${errorMsg}`);
+ }
+
+ if (!pdfData || pdfData.length === 0) {
+ throw new Error('Typst produced empty PDF output');
+ }
+
+ const pdfBlob = new Blob([pdfData], { type: 'application/pdf' });
+ this.output = pdfBlob;
+ this.displayResults(typstResult, options, files);
+ } else {
+ const result = await window.pandocModule.pandoc(options, stdin, files);
+ this.displayResults(result, options, files);
+ }
+ } catch (err) {
+ this.showOutput = true;
+ this.messages.push({ type: 'error', text: `Conversion failed: ${err.message}` });
+ } finally {
+ this.isConverting = false;
+ }
+ },
+
+ displayResults(result, options, files) {
+ this.showOutput = true;
+
+ if (result.warnings && result.warnings.length > 0) {
+ result.warnings.forEach(w => {
+ const msgType = w.verbosity === 'INFO' ? 'info' : 'warning';
+ const icon = w.verbosity === 'INFO' ? 'info' : 'warning';
+ this.messages.push({ type: msgType, text: w.pretty || w.message || JSON.stringify(w) });
+ });
+ }
+
+ if (result.stderr && result.stderr.includes('ERROR')) {
+ this.messages.push({ type: 'error', text: result.stderr });
+ return;
+ }
+
+ const isBinary = this.binaryFormats.includes(options.to);
+
+ if (this.output) {
+ // Already set (e.g., pdf via typst)
+ } else if (options['output-file'] && files[options['output-file']]) {
+ this.output = files[options['output-file']];
+ if (!isBinary) {
+ const reader = new FileReader();
+ reader.onload = () => {
+ this.outputPreview = reader.result.substring(0, 50000);
+ if (reader.result.length > 50000) {
+ this.outputPreview += '\n\n... (truncated)';
+ }
+ };
+ reader.readAsText(this.output);
+ }
+ } else if (result.stdout) {
+ this.output = new Blob([result.stdout], { type: 'text/plain' });
+ if (!isBinary) {
+ this.outputPreview = result.stdout;
+ }
+ }
+ },
+
+ download() {
+ if (!this.output) return;
+ const url = URL.createObjectURL(this.output);
+ const a = document.createElement('a');
+ a.href = url;
+ a.download = this.outputFilenameActual || 'output';
+ a.click();
+ URL.revokeObjectURL(url);
+ },
+
+ async copyToClipboard() {
+ try {
+ await navigator.clipboard.writeText(this.outputPreview);
+ this.copyBtnText = '✓ Copied!';
+ setTimeout(() => { this.copyBtnText = '📋 Copy to Clipboard'; }, 2000);
+ } catch (err) {
+ this.messages.push({ type: 'error', text: 'Failed to copy to clipboard' });
+ }
+ }
+ };
+ };
+
+ // Mount Petite Vue after module loads (pandoc.wasm is ready)
+ // Using mount() without selector scans DOM for v-scope attributes
+ PetiteVue.createApp().mount();
+ </script>
+</body>
+</html>
diff --git a/wasm/pandoc.js b/wasm/pandoc.js
new file mode 100644
index 000000000..3d3fee96a
--- /dev/null
+++ b/wasm/pandoc.js
@@ -0,0 +1,141 @@
+/* pandoc.js: JavaScript interface to pandoc.wasm.
+ Copyright (c) 2025 Tweag I/O Limited and John MacFarlane. MIT License.
+
+ Interface: await pandoc(options, stdin, files)
+
+ - options is a JavaScript object representing pandoc options: this should
+ correspond to the format used in pandoc's default files.
+ - stdin is a string or nil
+ - files is a JavaScript object whose keys are filenames and whose values
+ are the data in the corresponding file, as Blobs.
+
+ The return value is a JavaScript object with 3 properties, stdout, stderr,
+ and warnings, all strings. warnings is a JSON-encoded version of the warnings
+ produced by pandoc. If the pandoc process produces an output file, it will be
+ added to files.
+*/
+
+import {
+ WASI,
+ OpenFile,
+ File,
+ ConsoleStdout,
+ PreopenDirectory,
+} from "https://cdn.jsdelivr.net/npm/@bjorn3/[email protected]/dist/index.js";
+
+const args = ["pandoc.wasm", "+RTS", "-H64m", "-RTS"];
+const env = [];
+const in_file = new File(new Uint8Array(), { readonly: true });
+const out_file = new File(new Uint8Array(), { readonly: false });
+const err_file = new File(new Uint8Array(), { readonly: false });
+const warnings_file = new File(new Uint8Array(), { readonly: false });
+const fileSystem = new Map();
+const fds = [
+ new OpenFile(new File(new Uint8Array(), { readonly: true })),
+ ConsoleStdout.lineBuffered((msg) => console.log(`[WASI stdout] ${msg}`)),
+ ConsoleStdout.lineBuffered((msg) => console.warn(`[WASI stderr] ${msg}`)),
+ new PreopenDirectory("/", fileSystem),
+];
+const options = { debug: false };
+const wasi = new WASI(args, env, fds, options);
+const { instance } = await WebAssembly.instantiateStreaming(
+ fetch("./pandoc.wasm?sha1=XX"),
+ {
+ wasi_snapshot_preview1: wasi.wasiImport,
+ }
+);
+
+wasi.initialize(instance);
+instance.exports.__wasm_call_ctors();
+
+function memory_data_view() {
+ return new DataView(instance.exports.memory.buffer);
+}
+
+const argc_ptr = instance.exports.malloc(4);
+memory_data_view().setUint32(argc_ptr, args.length, true);
+const argv = instance.exports.malloc(4 * (args.length + 1));
+for (let i = 0; i < args.length; ++i) {
+ const arg = instance.exports.malloc(args[i].length + 1);
+ new TextEncoder().encodeInto(
+ args[i],
+ new Uint8Array(instance.exports.memory.buffer, arg, args[i].length)
+ );
+ memory_data_view().setUint8(arg + args[i].length, 0);
+ memory_data_view().setUint32(argv + 4 * i, arg, true);
+}
+memory_data_view().setUint32(argv + 4 * args.length, 0, true);
+const argv_ptr = instance.exports.malloc(4);
+memory_data_view().setUint32(argv_ptr, argv, true);
+
+instance.exports.hs_init_with_rtsopts(argc_ptr, argv_ptr);
+
+export async function getExtensionsForFormat(options) {
+ const opts_str = JSON.stringify(options);
+ const opts_ptr = instance.exports.malloc(opts_str.length);
+ new TextEncoder().encodeInto(
+ opts_str,
+ new Uint8Array(instance.exports.memory.buffer, opts_ptr, opts_str.length)
+ );
+ // add input files to fileSystem
+ fileSystem.clear()
+ const out_file = new File(new Uint8Array(), { readonly: false });
+ 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);
+
+ return JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(out_file.data));
+}
+
+export async function pandoc(options, stdin, files) {
+ const opts_str = JSON.stringify(options);
+ const opts_ptr = instance.exports.malloc(opts_str.length);
+ new TextEncoder().encodeInto(
+ opts_str,
+ new Uint8Array(instance.exports.memory.buffer, opts_ptr, opts_str.length)
+ );
+ // add input files to fileSystem
+ fileSystem.clear()
+ const in_file = new File(new Uint8Array(), { readonly: true });
+ const out_file = new File(new Uint8Array(), { readonly: false });
+ const err_file = new File(new Uint8Array(), { readonly: false });
+ const warnings_file = new File(new Uint8Array(), { readonly: false });
+ fileSystem.set("stdin", in_file);
+ fileSystem.set("stdout", out_file);
+ fileSystem.set("stderr", err_file);
+ fileSystem.set("warnings", warnings_file);
+ for (const file in files) {
+ await addFile(file, files[file], true);
+ }
+ // add output file if any
+ if (options["output-file"]) {
+ await addFile(options["output-file"], new Blob(), false);
+ }
+ if (stdin) {
+ in_file.data = new TextEncoder().encode(stdin);
+ }
+ instance.exports.wasm_main(opts_ptr, opts_str.length);
+
+ if (options["output-file"]) {
+ files[options["output-file"]] =
+ new Blob([fileSystem.get(options["output-file"]).data]);
+ }
+ const rawWarnings = new TextDecoder("utf-8", { fatal: true })
+ .decode(warnings_file.data);
+ let warnings = [];
+ if (rawWarnings) {
+ warnings = JSON.parse(rawWarnings);
+ }
+ return {
+ stdout: new TextDecoder("utf-8", { fatal: true }).decode(out_file.data),
+ stderr: new TextDecoder("utf-8", { fatal: true }).decode(err_file.data),
+ warnings: warnings
+ };
+}
+
+async function addFile(filename, blob, readonly) {
+ const buffer = await blob.arrayBuffer();
+ const file = new File(new Uint8Array(buffer), { readonly: readonly });
+ fileSystem.set(filename, file);
+}