packages feed

pandoc-3.2: src/Text/Pandoc/Readers/CommonMark.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{- |
   Module      : Text.Pandoc.Readers.CommonMark
   Copyright   : Copyright (C) 2015-2024 John MacFarlane
   License     : GNU GPL, version 2 or above

   Maintainer  : John MacFarlane <jgm@berkeley.edu>
   Stability   : alpha
   Portability : portable

Conversion of CommonMark-formatted plain text to 'Pandoc' document.

CommonMark is a strongly specified variant of Markdown: http://commonmark.org.
-}
module Text.Pandoc.Readers.CommonMark (readCommonMark)
where

import Commonmark
import Commonmark.Extensions
import Commonmark.Pandoc
import Data.Text (Text)
import Text.Pandoc.Class.PandocMonad (PandocMonad)
import Text.Pandoc.Definition
import Text.Pandoc.Builder as B
import Text.Pandoc.Options
import Text.Pandoc.Readers.Metadata (yamlMetaBlock)
import Control.Monad (MonadPlus(mzero))
import Control.Monad.Except (  MonadError(throwError) )
import Data.Functor.Identity (runIdentity)
import Data.Typeable
import Text.Pandoc.Parsing (runParserT, getInput, getPosition,
                            runF, defaultParserState, option, many1, anyChar,
                            Sources(..), ToSources(..), ParsecT, Future,
                            incSourceLine, fromParsecError)
import Text.Pandoc.Walk (walk)
import qualified Data.Text as T
import qualified Data.Attoparsec.Text as A
import Control.Applicative ((<|>))

-- | Parse a CommonMark formatted string into a 'Pandoc' structure.
readCommonMark :: (PandocMonad m, ToSources a)
               => ReaderOptions -> a -> m Pandoc
readCommonMark opts s
  | isEnabled Ext_yaml_metadata_block opts = do
    let sources = toSources s
    let firstSourceName = case unSources sources of
                               ((pos,_):_) -> sourceName pos
                               _ -> ""
    let toks = concatMap sourceToToks (unSources sources)
    res <- runParserT (do meta <- yamlMetaBlock (metaValueParser opts)
                          pos <- getPosition
                          rest <- getInput
                          let rest' = case rest of
                                -- update position of first source (#7863):
                                Sources ((_,t):xs) -> Sources ((pos,t):xs)
                                _ -> rest
                          return (meta, rest'))
                      defaultParserState firstSourceName sources
    case res of
      Left _ -> readCommonMarkBody opts sources toks
      Right (meta, rest) -> do
        -- strip off metadata section and parse body
        let body = concatMap sourceToToks (unSources rest)
        Pandoc _ bs <- readCommonMarkBody opts sources body
        return $ Pandoc (runF meta defaultParserState) bs
  | otherwise = do
    let sources = toSources s
    let toks = concatMap sourceToToks (unSources sources)
    readCommonMarkBody opts sources toks

makeFigures :: Block -> Block
makeFigures (Para [Image (ident,classes,kvs) alt (src,tit)]) =
  Figure (ident,[],[])
    (Caption Nothing [Plain alt])
    [Plain [Image ("",classes,kvs) alt (src,tit)]]
makeFigures b = b

sourceToToks :: (SourcePos, Text) -> [Tok]
sourceToToks (pos, s) = map adjust $ tokenize (sourceName pos) s
 where
   adjust = case sourceLine pos of
              1 -> id
              n -> \tok -> tok{ tokPos =
                                  incSourceLine (tokPos tok) (n - 1) }


metaValueParser :: Monad m
                => ReaderOptions -> ParsecT Sources st m (Future st MetaValue)
metaValueParser opts = do
  inp <- option "" $ T.pack <$> many1 anyChar
  let toks = concatMap sourceToToks (unSources (toSources inp))
  case runIdentity (parseCommonmarkWith (specFor opts) toks) of
     Left _ -> mzero
     Right (Cm bls :: Cm () Blocks) -> return $ return $ B.toMetaValue bls

readCommonMarkBody :: PandocMonad m => ReaderOptions -> Sources -> [Tok] -> m Pandoc
readCommonMarkBody opts s toks =
  (if isEnabled Ext_implicit_figures opts
      then walk makeFigures
      else id) .
  (if isEnabled Ext_tex_math_gfm opts
      then walk handleGfmMath
      else id) .
  (if readerStripComments opts
      then walk stripBlockComments . walk stripInlineComments
      else id) <$>
  if isEnabled Ext_sourcepos opts
     then case runIdentity (parseCommonmarkWith (specFor opts) toks) of
            Left err -> throwError $ fromParsecError s err
            Right (Cm bls :: Cm SourceRange Blocks) -> return $ B.doc bls
     else case runIdentity (parseCommonmarkWith (specFor opts) toks) of
            Left err -> throwError $ fromParsecError s err
            Right (Cm bls :: Cm () Blocks) -> return $ B.doc bls

handleGfmMath :: Block -> Block
handleGfmMath (CodeBlock ("",["math"],[]) raw) = Para [Math DisplayMath raw]
handleGfmMath x = walk handleGfmMathInline x

handleGfmMathInline :: Inline -> Inline
handleGfmMathInline (Math InlineMath math') =
  let (ticks, rest) = T.span (== '`') math'
  in  if T.null ticks
         then Math InlineMath math'
         else case T.stripSuffix ticks rest of
                Just middle | not (T.null middle) && (T.last middle /= '`')
                             -> Math InlineMath middle
                _ -> Math InlineMath math'
handleGfmMathInline x = x

stripBlockComments :: Block -> Block
stripBlockComments (RawBlock (B.Format "html") s) =
  RawBlock (B.Format "html") (removeComments s)
stripBlockComments x = x

stripInlineComments :: Inline -> Inline
stripInlineComments (RawInline (B.Format "html") s) =
  RawInline (B.Format "html") (removeComments s)
stripInlineComments x = x

removeComments :: Text -> Text
removeComments s =
  either (const s) id $ A.parseOnly pRemoveComments s
 where
  pRemoveComments = mconcat <$> A.many'
    ("" <$ (A.string "<!--" *> A.scan (0 :: Int) scanChar <* A.char '>') <|>
     (A.takeWhile1 (/= '<')) <|>
     (A.string "<"))
  scanChar st c =
    case c of
      '-' -> Just (st + 1)
      '>' | st >= 2 -> Nothing
      _ -> Just 0

specFor :: (Monad m, Typeable m, Typeable a,
            Rangeable (Cm a Inlines), Rangeable (Cm a Blocks))
        => ReaderOptions -> SyntaxSpec m (Cm a Inlines) (Cm a Blocks)
specFor opts = foldr ($) defaultSyntaxSpec exts
 where
  exts = [ (hardLineBreaksSpec <>) | isEnabled Ext_hard_line_breaks opts ] ++
         [ (smartPunctuationSpec <>) | isEnabled Ext_smart opts ] ++
         [ (strikethroughSpec <>) | isEnabled Ext_strikeout opts ] ++
         [ (superscriptSpec <>) | isEnabled Ext_superscript opts ] ++
         [ (subscriptSpec <>) | isEnabled Ext_subscript opts ] ++
         [ (mathSpec <>) | isEnabled Ext_tex_math_dollars opts ] ++
         [ (fancyListSpec <>) | isEnabled Ext_fancy_lists opts ] ++
         [ (fencedDivSpec <>) | isEnabled Ext_fenced_divs opts ] ++
         [ (bracketedSpanSpec <>) | isEnabled Ext_bracketed_spans opts ] ++
         [ (rawAttributeSpec <>) | isEnabled Ext_raw_attribute opts ] ++
         [ (attributesSpec <>) | isEnabled Ext_attributes opts ] ++
         [ (alertSpec <>) | isEnabled Ext_alerts opts ] ++
         [ (<> pipeTableSpec) | isEnabled Ext_pipe_tables opts ] ++
            -- see #6739
         [ (autolinkSpec <>) | isEnabled Ext_autolink_bare_uris opts ] ++
         [ (emojiSpec <>) | isEnabled Ext_emoji opts ] ++
         [ (autoIdentifiersSpec <>)
           | isEnabled Ext_gfm_auto_identifiers opts
           , not (isEnabled Ext_ascii_identifiers opts) ] ++
         [ (autoIdentifiersAsciiSpec <>)
           | isEnabled Ext_gfm_auto_identifiers opts
           , isEnabled Ext_ascii_identifiers opts ] ++
         [ (implicitHeadingReferencesSpec <>)
           | isEnabled Ext_implicit_header_references opts ] ++
         [ (footnoteSpec <>) | isEnabled Ext_footnotes opts ] ++
         [ (definitionListSpec <>) | isEnabled Ext_definition_lists opts ] ++
         [ (taskListSpec <>) | isEnabled Ext_task_lists opts ] ++
         [ (wikilinksSpec TitleAfterPipe <>)
           | isEnabled Ext_wikilinks_title_after_pipe opts ] ++
         [ (wikilinksSpec TitleBeforePipe <>)
           | isEnabled Ext_wikilinks_title_before_pipe opts ] ++
         [ (rebaseRelativePathsSpec <>)
           | isEnabled Ext_rebase_relative_paths opts ]