packages feed

pandoc-3.1.12: src/Text/Pandoc/Writers/Djot.hs

{-# 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 (StateT(..), gets, modify)
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