diff options
| author | John MacFarlane <[email protected]> | 2026-01-19 20:58:18 +0100 |
|---|---|---|
| committer | John MacFarlane <[email protected]> | 2026-01-19 20:58:18 +0100 |
| commit | 583de2a9804aba81282e360e87805adad73f0a49 (patch) | |
| tree | f52602445911da0e43c87b00e12a36995980ad6e | |
| parent | 49aa337c1dfe09111ebdde16aebd8f106a7e152e (diff) | |
Add support for compiling pandoc.wasm.
'make pandoc.wasm' will compile a wasm version of pandoc.
This commit also adds a wasm directory with pandoc.js, a
JavaScript wrapper for pandoc.wasm, and an example application
that exposes almost all pandoc features to a web interface,
together with a set of examples.
53 files changed, 5434 insertions, 20 deletions
@@ -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", @@ -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 Binary files differnew file mode 100644 index 000000000..26dd2daaa --- /dev/null +++ b/wasm/examples/docx-with-equations-to-latex/equations.docx 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  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. 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=" (" 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=" (" 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=" (" 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=" (" 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=" "> + <label variable="locator" form="short"/> + <text variable="locator"/> + </group> + </if> + <else-if variable="locator" match="none"> + <text variable="number-of-pages" suffix=" 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=" "> + <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=" "> + <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=" "> + <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=" 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=" "/> + </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=". "/> + <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=" "/> + <text variable="issue"/> + </if> + <else> + <text variable="issue"/> + </else> + </choose> + </macro> + <macro name="collection"> + <text variable="collection-title" quotes="true" prefix=" (coll. " suffix=")"/> + </macro> + <citation et-al-min="4" et-al-use-first="1"> + <layout suffix="." delimiter=" ; "> + <choose> + <if position="ibid-with-locator"> + <group delimiter=", "> + <text term="ibid" text-case="capitalize-first" font-style="italic" suffix="."/> + <text variable="locator" prefix="p. "/> + </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. "/> + </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. + +{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 ? '▼' : '▶' }} 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">📄</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">📄</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)">✕</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">🖼</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)">✕</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">📄 <a href="#" @click.prevent="downloadFile(bibFile, bibFile.name)">{{ bibFile.name }}</a> <span class="file-remove" @click="bibFile = null">✕</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">📄 <a href="#" @click.prevent="downloadFile(cslFile, cslFile.name)">{{ cslFile.name }}</a> <span class="file-remove" @click="cslFile = null">✕</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">📄 <a href="#" @click.prevent="downloadFile(abbrevFile, abbrevFile.name)">{{ abbrevFile.name }}</a> <span class="file-remove" @click="abbrevFile = null">✕</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">🎨 <a href="#" @click.prevent="downloadFile(highlightThemeFile, highlightThemeFile.name)">{{ highlightThemeFile.name }}</a> <span class="file-remove" @click="highlightThemeFile = null">✕</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">📄</span> + <a class="file-name" href="#" @click.prevent="downloadFile(file, file.name)">{{ file.name }}</a> + <span class="file-remove" @click="removeSyntaxDef(file.name)">✕</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 <q> 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 <section> 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">🎨</span> + <a class="file-name" href="#" @click.prevent="downloadFile(file, file.name)">{{ file.name }}</a> + <span class="file-remove" @click="removeCssFile(file.name)">✕</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">🖼 <a href="#" @click.prevent="downloadFile(epubCoverImage, epubCoverImage.name)">{{ epubCoverImage.name }}</a> <span class="file-remove" @click="epubCoverImage = null">✕</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">🔠</span> + <a class="file-name" href="#" @click.prevent="downloadFile(file, file.name)">{{ file.name }}</a> + <span class="file-remove" @click="removeEpubFont(file.name)">✕</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">📄 <a href="#" @click.prevent="downloadFile(templateFile, templateFile.name)">{{ templateFile.name }}</a> <span class="file-remove" @click="templateFile = null">✕</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">📄 <a href="#" @click.prevent="downloadFile(referenceDoc, referenceDoc.name)">{{ referenceDoc.name }}</a> <span class="file-remove" @click="referenceDoc = null">✕</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">📄</span> + <a class="file-name" href="#" @click.prevent="downloadFile(file, file.name)">{{ file.name }}</a> + <span class="file-remove" @click="removeHeaderFile(file.name)">✕</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">📄</span> + <a class="file-name" href="#" @click.prevent="downloadFile(file, file.name)">{{ file.name }}</a> + <span class="file-remove" @click="removeBeforeBodyFile(file.name)">✕</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">📄</span> + <a class="file-name" href="#" @click.prevent="downloadFile(file, file.name)">{{ file.name }}</a> + <span class="file-remove" @click="removeAfterBodyFile(file.name)">✕</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)">✕</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">📄 <a href="#" @click.prevent="downloadFile(metadataFile, metadataFile.name)">{{ metadataFile.name }}</a> <span class="file-remove" @click="metadataFile = null">✕</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)">✕</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">📥 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); +} |
