packages feed

commonmark-cli-0.1.0.0: src/convert.hs

{-# LANGUAGE CPP                 #-}
{-# LANGUAGE BangPatterns        #-}
{-# LANGUAGE OverloadedStrings   #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Main where

import           Commonmark
import           Commonmark.Extensions
import           Commonmark.Pandoc
import           Data.Maybe                 (isJust)
import           Data.Aeson                 (encode)
import qualified Data.Sequence              as Seq
import qualified Data.ByteString.Lazy       as BL
import qualified Text.Pandoc.Builder        as B
import           Control.Monad
import           Control.Monad.Identity
import           Control.Monad.State
import           Data.Typeable
import qualified Data.Text.IO               as TIO
import qualified Data.Text.Lazy.IO          as TLIO
import qualified Data.Map                   as M
import qualified Data.Text                  as T
import           System.Environment
import           System.Exit
import           System.IO
import           System.Console.GetOpt
import           Paths_commonmark_cli (version)
import           Data.Version (showVersion)
#if !MIN_VERSION_base(4,11,0)
import           Data.Monoid
#endif
import           Control.Exception          (AsyncException, catch, throwIO)
import           GHC.Stack                  (currentCallStack)
import           System.Console.ANSI

data Opt =
       Help
     | Version
     | Tokenize
     | SourcePos
     | Highlight
     | PandocJSON
     | ListExtensions
     | Extension String
     deriving Eq

options :: [OptDescr Opt]
options =
  [ Option ['t'] ["tokenize"] (NoArg Tokenize) "tokenize"
  , Option ['x'] ["extension"] (ReqArg Extension "extension") "use extension"
  , Option ['p'] ["sourcepos"] (NoArg SourcePos) "source positions"
  , Option [] ["highlight"] (NoArg Highlight) "highlight using ANSI"
  , Option ['j'] ["json"] (NoArg PandocJSON) "pandoc JSON output"
  , Option ['v'] ["version"] (NoArg Version) "version info"
  , Option [] ["list-extensions"] (NoArg ListExtensions) "list extensions"
  , Option ['h'] ["help"] (NoArg Help) "help message"
  ]

usageMessage :: String -> [OptDescr Opt] -> String
usageMessage programName = usageInfo (programName ++ " [OPTIONS] [FILES]")

main :: IO ()
main = catch (do
  (opts, files, errs) <- getOpt Permute options <$> getArgs
  unless (null errs) $ do
    mapM_ (hPutStrLn stderr) errs
    exitWith (ExitFailure 1)
  prg <- getProgName
  when (Help `elem` opts) $ do
    putStr (usageMessage prg options)
    exitSuccess
  when (ListExtensions `elem` opts) $ do
    listExtensions
    exitSuccess
  when (Version `elem` opts) $ do
    putStrLn $ prg ++ " " ++ showVersion version
    exitSuccess
  toks <- if null files
            then tokenize "stdin" <$> TIO.getContents
            else mconcat <$> mapM (\f -> tokenize f <$> TIO.readFile f) files
  when (Tokenize `elem` opts) $ do
    print toks
    exitSuccess
  if Highlight `elem` opts then do
      spec <- specFromExtensionNames [x | Extension x <- opts]
      case runWithSourceMap <$>
              runIdentity (parseCommonmarkWith spec toks) of
           Left e -> errExit e
           Right (_ :: Html (), sm) -> highlightWith sm toks
  else
    if SourcePos `elem` opts then do
       spec <- specFromExtensionNames [x | Extension x <- opts]
       case runIdentity (parseCommonmarkWith spec toks) of
            Left e -> errExit e
            Right (r :: Html SourceRange)
                   -> TLIO.putStr . renderHtml $ r
    else
      if PandocJSON `elem` opts then do
        spec <- specFromExtensionNames [x | Extension x <- opts]
        case runIdentity (parseCommonmarkWith spec toks) of
             Left e -> errExit e
             Right (r :: Cm () B.Blocks) -> do
               BL.putStr . encode $ B.doc $ unCm r
               BL.putStr "\n"
      else do
        spec <- specFromExtensionNames [x | Extension x <- opts]
        case runIdentity (parseCommonmarkWith spec toks) of
             Left e -> errExit e
             Right (r :: Html ()) -> TLIO.putStr . renderHtml $ r)
   (\(e :: AsyncException) -> do
             currentCallStack >>= mapM_ (hPutStrLn stderr)
             throwIO e)


errExit :: ParseError -> IO a
errExit err = do
  hPrint stderr err
  exitWith (ExitFailure 1)

extensions :: (Monad m, Typeable m,
               Typeable bl, Typeable il,
               IsBlock il bl, IsInline il,
               HasPipeTable il bl,
               HasMath il,
               HasEmoji il,
               HasSpan il,
               ToPlainText il,
               HasStrikethrough il,
               HasSuperscript il,
               HasSubscript il,
               HasDefinitionList il bl,
               HasDiv bl,
               HasTaskList il bl,
               HasFootnote il bl)
           => [(String, SyntaxSpec m il bl)]
extensions =
  [ ("autolinks", autolinkSpec)
  ,("pipe_tables", pipeTableSpec)
  ,("hard_line_breaks", hardLineBreaksSpec)
  ,("strikethrough", strikethroughSpec)
  ,("superscript", superscriptSpec)
  ,("subscript", subscriptSpec)
  ,("smart", smartPunctuationSpec)
  ,("math", mathSpec)
  ,("emoji", emojiSpec)
  ,("footnotes", footnoteSpec)
  ,("definition_lists", definitionListSpec)
  ,("fancy_lists", fancyListSpec)
  ,("task_lists", taskListSpec)
  ,("attributes", attributesSpec)
  ,("raw_attribute", rawAttributeSpec)
  ,("bracketed_spans", bracketedSpanSpec)
  ,("fenced_divs", fencedDivSpec)
  ,("auto_identifiers", autoIdentifiersSpec)
  ,("auto_identifiers_ascii", autoIdentifiersAsciiSpec)
  ,("implicit_heading_references", implicitHeadingReferencesSpec)
  ,("gfm", gfmExtensions)
  ]

extensionList :: [String]
extensionList = map fst
  (extensions :: [(String, SyntaxSpec IO (Html ()) (Html ()))])

listExtensions :: IO ()
listExtensions =
  putStr $ "Available extensions (or 'all' for all):\n" ++
    unlines (map ("  " ++) extensionList)

specFromExtensionNames ::
 (Monad m, Typeable m, Typeable bl, Typeable il,
  IsBlock il bl, IsInline il,
  HasPipeTable il bl, HasMath il, HasEmoji il,
  HasSpan il,
  ToPlainText il,
  HasStrikethrough il,
  HasSuperscript il,
  HasSubscript il,
  HasDefinitionList il bl,
  HasDiv bl,
  HasTaskList il bl,
  HasFootnote il bl)
  => [String] -> IO (SyntaxSpec m il bl)
specFromExtensionNames extnames = do
 let extFromName name =
       case lookup name extensions of
         Just ext -> return ext
         Nothing  -> do
           hPutStrLn stderr $ "Unknown extension " ++ name
           listExtensions
           exitWith (ExitFailure 1)
 exts <- if "all" `elem` extnames
            then return $ mconcat (map snd extensions)
            else mconcat <$> mapM extFromName extnames
 return $ exts <> defaultSyntaxSpec

highlightWith :: SourceMap -> [Tok] -> IO ()
highlightWith sm ts = evalStateT (mapM_ (hlTok sm) ts) (mempty, [])

hlTok :: SourceMap -> Tok
      -> StateT (Seq.Seq T.Text, [SGR]) IO ()
hlTok (SourceMap !sm) (Tok toktype !pos !t) = do
  (xs, sgrs) <- get
  -- When we encounter a line end, we store it in mbLineEnd
  -- and output it after the formatting codes at the beginning
  -- of the next line.  Otherwise the wrong lines are affected.
  case M.lookup pos sm of
       Nothing -> liftIO $ do
         when (toktype == LineEnd) $
           -- This escape sequence paints rest of line with current
           -- background color; otherwise we get bad results with scrolling.
           TIO.putStr "\27[K"
         TIO.putStr t
       Just (starts, ends) -> do
         let xsMinusEnds = foldr (\e s ->
                             case Seq.viewr s of
                               Seq.EmptyR -> s
                               z Seq.:> x
                                  | x == e -> z
                                  | otherwise -> s) xs (Seq.reverse ends)
         let xs' = xsMinusEnds <> starts
         let isStr = xs' `has` "str"
         let sgrs' = if xs == xs'
                        then sgrs
                        else Reset :
                             SetConsoleIntensity
                               (if isStr
                                   then NormalIntensity
                                   else FaintIntensity) :
                             foldMap sgrFrom xs'
         put (xs', sgrs')
         liftIO $ do
            when (sgrs /= sgrs') $ setSGR sgrs'
            when (toktype == LineEnd) $
              TIO.putStr "\27[K"
            TIO.putStr t

sgrFrom :: T.Text -> [SGR]
sgrFrom t =
  case t of
     "link"          -> [SetUnderlining SingleUnderline]
     "image"         -> [SetColor Foreground Vivid Magenta]
     "entity"        -> [SetColor Foreground Vivid Magenta]
     "escapedChar"   -> [SetColor Foreground Vivid Magenta]
     "code"          -> [SetColor Foreground Vivid White,
                         SetColor Background Dull Cyan]
     "codeBlock"     -> [SetColor Foreground Vivid White,
                         SetColor Background Dull Cyan]
     "rawInline"     -> [SetColor Foreground Vivid Green]
     "rawBlock"      -> [SetColor Foreground Vivid Green]
     "heading1"      -> [SetColor Foreground Vivid Blue]
     "heading2"      -> [SetColor Foreground Vivid Blue]
     "heading3"      -> [SetColor Foreground Vivid Blue]
     "heading4"      -> [SetColor Foreground Vivid Blue]
     "heading5"      -> [SetColor Foreground Vivid Blue]
     "heading6"      -> [SetColor Foreground Vivid Blue]
     "blockQuote"    -> [SetColor Foreground Dull Cyan]
     "referenceLinkDefinition"
                     -> [SetColor Foreground Dull Cyan,
                         SetUnderlining SingleUnderline]
     "emoji"         -> [SetColor Foreground Vivid Magenta]
     "math"          -> [SetColor Foreground Vivid Magenta]
     "strikethrough" -> [SetColor Foreground Vivid Magenta]
     "superscript"   -> [SetColor Foreground Vivid Magenta]
     "subscript"     -> [SetColor Foreground Vivid Magenta]
     "footnote"      -> [SetColor Foreground Vivid Magenta]
     "footnoteRef"   -> [SetColor Foreground Vivid Magenta]
     _               -> []

has :: Seq.Seq T.Text -> T.Text -> Bool
has xs t = isJust $ t `Seq.elemIndexL` xs