summaryrefslogtreecommitdiff
path: root/src/Text/Pandoc/Writers/Djot.hs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Text/Pandoc/Writers/Djot.hs')
-rw-r--r--src/Text/Pandoc/Writers/Djot.hs296
1 files changed, 296 insertions, 0 deletions
diff --git a/src/Text/Pandoc/Writers/Djot.hs b/src/Text/Pandoc/Writers/Djot.hs
new file mode 100644
index 000000000..0e605398e
--- /dev/null
+++ b/src/Text/Pandoc/Writers/Djot.hs
@@ -0,0 +1,296 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{- |
+ Module : Text.Pandoc.Writers.Djot
+ Copyright : Copyright (C) 2024 John MacFarlane
+ License : GNU GPL, version 2 or above
+
+ Maintainer : John MacFarlane <jgm@berkeley.edu>
+ Stability : alpha
+ Portability : portable
+
+Conversion of 'Pandoc' format into Djot markup (<https://djot.net>).
+-}
+module Text.Pandoc.Writers.Djot (
+ writeDjot
+ ) where
+import Text.Pandoc.Definition
+import Text.Pandoc.Logging
+import Text.Pandoc.Class ( PandocMonad , report )
+import Text.Pandoc.Options ( WriterOptions(..), WrapOption(..))
+import Data.Text (Text)
+import Data.Set (Set)
+import qualified Data.Set as Set
+import qualified Data.ByteString as B
+import qualified Data.ByteString.Char8 as B8
+import Data.List (intersperse)
+import qualified Data.Text as T
+import qualified Data.Map as M
+import qualified Text.Pandoc.UTF8 as UTF8
+import Text.Pandoc.Writers.Shared ( metaToContext, defField, toLegacyTable )
+import Text.Pandoc.Shared (isTightList, tshow, stringify, onlySimpleTableCells,
+ makeSections)
+import Text.DocLayout
+import Text.DocTemplates (renderTemplate)
+
+import Control.Monad.State
+import Control.Monad (zipWithM, when)
+import Data.Maybe (fromMaybe)
+import qualified Djot.AST as D
+import Djot (renderDjot, RenderOptions(..), toIdentifier)
+import Text.Pandoc.UTF8 (fromText)
+
+-- | Convert Pandoc to Djot.
+writeDjot :: PandocMonad m => WriterOptions -> Pandoc -> m Text
+writeDjot opts (Pandoc meta blocks) = do
+ let colwidth = if writerWrapText opts == WrapAuto
+ then Just $ writerColumns opts
+ else Nothing
+ let ropts = RenderOptions{ preserveSoftBreaks =
+ writerWrapText opts == WrapPreserve }
+ metadata <- metaToContext opts
+ (fmap (renderDjot ropts) . bodyToDjot opts)
+ (fmap (chomp . renderDjot ropts) . bodyToDjot opts .
+ (:[]) . Plain)
+ meta
+ main <- renderDjot ropts <$>
+ bodyToDjot opts (makeSections False Nothing blocks)
+ let context = defField "body" main metadata
+ return $ render colwidth $
+ case writerTemplate opts of
+ Nothing -> main
+ Just tpl -> renderTemplate tpl context
+
+data DjotState =
+ DjotState
+ { footnotes :: D.NoteMap
+ , references :: D.ReferenceMap
+ , autoReferences :: D.ReferenceMap
+ , autoIds :: Set B.ByteString
+ , options :: WriterOptions }
+
+bodyToDjot :: PandocMonad m => WriterOptions -> [Block] -> m D.Doc
+bodyToDjot opts bls = do
+ (bs, st) <- runStateT (blocksToDjot bls)
+ (DjotState mempty mempty mempty mempty opts)
+ let D.ReferenceMap autos = autoReferences st
+ let D.ReferenceMap refs = references st
+ pure $ D.Doc{ D.docBlocks = bs
+ , D.docFootnotes = footnotes st
+ , D.docReferences = D.ReferenceMap $ M.difference refs autos
+ , D.docAutoReferences = D.ReferenceMap autos
+ , D.docAutoIdentifiers = autoIds st
+ }
+
+blocksToDjot :: PandocMonad m => [Block] -> StateT DjotState m D.Blocks
+blocksToDjot = fmap mconcat . mapM blockToDjot
+
+blockToDjot :: PandocMonad m => Block -> StateT DjotState m D.Blocks
+blockToDjot (Para ils) = D.para <$> inlinesToDjot ils
+blockToDjot (Plain ils) = D.para <$> inlinesToDjot ils
+blockToDjot (LineBlock ls) =
+ D.para . mconcat . intersperse D.hardBreak <$> mapM inlinesToDjot ls
+blockToDjot (CodeBlock attr@(_,_,kvs) t) = do
+ let lang = fromMaybe mempty $ lookup "lang" kvs
+ pure $ D.addAttr (toDjotAttr attr)
+ <$> D.codeBlock (fromText lang) (fromText t)
+blockToDjot (RawBlock (Format f) t) =
+ pure $ D.rawBlock (D.Format (fromText f)) (fromText t)
+blockToDjot (BlockQuote bls) = D.blockQuote <$> blocksToDjot bls
+blockToDjot (Header lev attr ils) =
+ fmap (D.addAttr (toDjotAttr attr)) . D.heading lev <$> inlinesToDjot ils
+blockToDjot HorizontalRule = pure D.thematicBreak
+blockToDjot (Div (ident,"section":cls,kvs) bls@(Header _ _ ils : _)) = do
+ ilsBs <- D.inlinesToByteString <$> inlinesToDjot ils
+ let ident' = toIdentifier ilsBs
+ let label = D.normalizeLabel ilsBs
+ let autoid = UTF8.toText ident' == ident
+ when autoid $
+ modify $ \st -> st{ autoIds = Set.insert ident' (autoIds st) }
+ modify $ \st -> st{ autoReferences = D.insertReference label
+ (B8.cons '#' ident', mempty) (autoReferences st) }
+ fmap
+ (D.addAttr (toDjotAttr (if autoid then "" else ident,
+ filter (/= "section") cls,
+ filter (\(k,_) -> k /= "wrapper") kvs))) . D.section
+ <$> blocksToDjot bls
+blockToDjot (Div attr@(ident,cls,kvs) bls)
+ | Just "1" <- lookup "wrapper" kvs
+ = fmap (D.addAttr
+ (toDjotAttr (ident,cls,filter (\(k,_) -> k /= "wrapper") kvs)))
+ <$> blocksToDjot bls
+ | otherwise
+ = fmap (D.addAttr (toDjotAttr attr)) . D.div <$> blocksToDjot bls
+blockToDjot (BulletList items) =
+ D.bulletList spacing <$> mapM blocksToDjot items
+ where
+ spacing = if isTightList items then D.Tight else D.Loose
+blockToDjot (OrderedList (start, sty, delim) items) =
+ D.orderedList listAttr spacing <$> mapM blocksToDjot items
+ where
+ spacing = if isTightList items then D.Tight else D.Loose
+ listAttr = D.OrderedListAttributes {
+ D.orderedListStyle =
+ case sty of
+ DefaultStyle -> D.Decimal
+ Example -> D.Decimal
+ Decimal -> D.Decimal
+ LowerRoman -> D.RomanLower
+ UpperRoman -> D.RomanUpper
+ LowerAlpha -> D.LetterLower
+ UpperAlpha -> D.LetterUpper,
+ D.orderedListDelim =
+ case delim of
+ DefaultDelim -> D.RightPeriod
+ Period -> D.RightPeriod
+ OneParen -> D.RightParen
+ TwoParens -> D.LeftRightParen,
+ D.orderedListStart = start }
+blockToDjot (DefinitionList items) =
+ D.definitionList spacing <$> mapM toDLItem items
+ where
+ spacing = if isTightList (map (concat . snd) items)
+ then D.Tight
+ else D.Loose
+ toDLItem (term, defs) = do
+ term' <- inlinesToDjot term
+ def' <- mconcat <$> mapM blocksToDjot defs
+ pure (term', def')
+blockToDjot (Figure attr (Caption _ capt) bls) = do
+ content <- blocksToDjot bls
+ caption <- fmap (D.addAttr (D.Attr [("class","caption")])) . D.div <$>
+ blocksToDjot capt
+ pure $ fmap (D.addAttr (toDjotAttr attr)) $ D.div $ content <> caption
+blockToDjot (Table attr capt' colspecs thead tbodies tfoot) = do
+ let (capt, aligns, _, headRow, bodyRows) =
+ toLegacyTable capt' colspecs thead tbodies tfoot
+ if onlySimpleTableCells (headRow : bodyRows)
+ then do
+ let alignToAlign al = case al of
+ AlignDefault -> D.AlignDefault
+ AlignLeft -> D.AlignLeft
+ AlignRight -> D.AlignRight
+ AlignCenter -> D.AlignCenter
+ let defAligns = map alignToAlign aligns
+ let cellToCell isHeader bls al =
+ D.Cell (if isHeader then D.HeadCell else D.BodyCell) al
+ <$> case bls of
+ [Para ils] -> inlinesToDjot ils
+ [Plain ils] -> inlinesToDjot ils
+ [] -> pure mempty
+ bs -> do
+ mapM_ (report . BlockNotRendered) bs
+ pure $ D.str "((omitted))"
+ let rowToRow isHeader cells = zipWithM (cellToCell isHeader) cells defAligns
+ hrows <- if null headRow
+ then pure []
+ else (:[]) <$> rowToRow True headRow
+ rows <- mapM (rowToRow False) bodyRows
+ caption <- case capt of
+ [] -> pure Nothing
+ _ -> Just . D.Caption . D.para <$> inlinesToDjot capt
+ pure $ D.addAttr (toDjotAttr attr) <$> D.table caption (hrows <> rows)
+ else do -- table can't be represented as a simple pipe table, use list
+ tableList <- D.bulletList D.Loose <$> mapM
+ (fmap (D.bulletList D.Loose) . mapM blocksToDjot)
+ (headRow:bodyRows)
+ pure $ D.addAttr (D.Attr [("class", "table")]) <$> tableList
+
+inlinesToDjot :: PandocMonad m => [Inline] -> StateT DjotState m D.Inlines
+inlinesToDjot = fmap mconcat . mapM inlineToDjot
+
+inlineToDjot :: PandocMonad m => Inline -> StateT DjotState m D.Inlines
+inlineToDjot (Str t) = pure $ D.str (fromText t)
+inlineToDjot Space = pure $ D.str " "
+inlineToDjot SoftBreak = pure D.softBreak
+inlineToDjot LineBreak = pure D.hardBreak
+inlineToDjot (Emph ils) = D.emph <$> inlinesToDjot ils
+inlineToDjot (Underline ils) =
+ fmap (D.addAttr (D.Attr [("class","underline")])) . D.span_
+ <$> inlinesToDjot ils
+inlineToDjot (Strong ils) = D.strong <$> inlinesToDjot ils
+inlineToDjot (Strikeout ils) = D.delete <$> inlinesToDjot ils
+inlineToDjot (Subscript ils) = D.subscript <$> inlinesToDjot ils
+inlineToDjot (Superscript ils) = D.superscript <$> inlinesToDjot ils
+inlineToDjot (Span attr@(ident,cls,kvs) ils)
+ | Just "1" <- lookup "wrapper" kvs
+ = fmap (D.addAttr
+ (toDjotAttr (ident,cls,filter (\(k,_) -> k /= "wrapper") kvs)))
+ <$> inlinesToDjot ils
+ | otherwise
+ = fmap (D.addAttr (toDjotAttr attr)) . D.span_ <$> inlinesToDjot ils
+inlineToDjot (SmallCaps ils) =
+ fmap (D.addAttr (D.Attr [("class","smallcaps")])) . D.span_
+ <$> inlinesToDjot ils
+inlineToDjot (Quoted DoubleQuote ils) = D.doubleQuoted <$> inlinesToDjot ils
+inlineToDjot (Quoted SingleQuote ils) = D.singleQuoted <$> inlinesToDjot ils
+inlineToDjot (Cite _cs ils) = inlinesToDjot ils
+inlineToDjot (Code attr t) =
+ pure $ D.addAttr (toDjotAttr attr) <$> D.verbatim (fromText t)
+inlineToDjot (Math mt t) =
+ pure $ (if mt == InlineMath
+ then D.inlineMath
+ else D.displayMath) (fromText t)
+inlineToDjot (RawInline (Format f) t) =
+ pure $ D.rawInline (D.Format (fromText f)) (fromText t)
+inlineToDjot (Link attr ils (src,tit)) = do
+ opts <- gets options
+ description <- inlinesToDjot ils
+ let ilstring = stringify ils
+ let autolink = ilstring == src
+ let email = ("mailto:" <> ilstring) == src
+ let removeClass name (ident, cls, kvs) = (ident, filter (/= name) cls, kvs)
+ let attr' = D.Attr [("title", fromText tit) | not (T.null tit)] <>
+ toDjotAttr ( (if autolink
+ then removeClass "uri"
+ else id) .
+ (if email
+ then removeClass "email"
+ else id) $ attr)
+ case () of
+ _ | autolink -> pure $ D.addAttr attr' <$> D.urlLink (fromText ilstring)
+ | email -> pure $ D.addAttr attr' <$> D.emailLink (fromText ilstring)
+ | writerReferenceLinks opts
+ -> do refs@(D.ReferenceMap m) <- gets references
+ autoRefs <- gets autoReferences
+ let lab' = D.inlinesToByteString description
+ lab <- case D.lookupReference lab' (refs <> autoRefs) of
+ Just _ -> pure lab'
+ Nothing -> do
+ let refnum = M.size m + 1
+ let lab = fromText $ tshow refnum
+ modify $ \st -> st{ references =
+ D.insertReference lab
+ (fromText src, attr') refs }
+ pure lab
+ pure $ D.addAttr attr' <$> D.link description (D.Reference lab)
+ | otherwise
+ -> pure $ D.addAttr attr' <$> D.link description (D.Direct (fromText src))
+inlineToDjot (Image attr ils (src,tit)) = do
+ opts <- gets options
+ description <- inlinesToDjot ils
+ let attr' = D.Attr [("title", fromText tit) | not (T.null tit)] <>
+ toDjotAttr attr
+ if writerReferenceLinks opts
+ then do
+ refs@(D.ReferenceMap m) <- gets references
+ let refnum = M.size m + 1
+ let lab = fromText $ tshow refnum
+ modify $ \st -> st{ references =
+ D.insertReference lab
+ (fromText src, attr') refs }
+ pure $ D.addAttr attr' <$> D.image description (D.Reference lab)
+ else pure $ D.addAttr attr' <$> D.image description (D.Direct (fromText src))
+inlineToDjot (Note bs) = do
+ notes@(D.NoteMap m) <- gets footnotes
+ let notenum = M.size m + 1
+ let lab = fromText $ tshow notenum
+ contents <- blocksToDjot bs
+ modify $ \st -> st{ footnotes = D.insertNote lab contents notes }
+ pure $ D.footnoteReference lab
+
+toDjotAttr :: (Text, [Text], [(Text, Text)]) -> D.Attr
+toDjotAttr (ident, classes, kvs) =
+ D.Attr $ [("id", fromText ident) | not (T.null ident)] ++
+ [("class", fromText (T.unwords classes)) | not (null classes)] ++
+ map (\(k,v) -> (fromText k, fromText v)) kvs