packages feed

Agda-2.9.0: src/full/Agda/Syntax/Parser/Literate.hs

{-# OPTIONS_GHC -Wunused-imports #-}

-- | Preprocessors for literate code formats.

module Agda.Syntax.Parser.Literate
  ( literateProcessors
  , literateExts
  , literateTeX
  , literateRsT
  , literateMd
  , literateOrg
  , illiterate
  , atomizeLayers
  , Processor
  , Layers
  , Layer(..)
  , LayerRole(..)
  , isCode
  , isCodeLayer
  )
  where

import Control.Monad ((<=<))
import Data.Char (isSpace)
import Data.List (isPrefixOf)
import Text.Regex.TDFA
  ( Regex, getAllTextSubmatches, match, matchM
  , makeRegexOpts, blankCompOpt, blankExecOpt, newSyntax, caseSensitive
  )

import Agda.Syntax.Common
import Agda.Syntax.Position

import Agda.Utils.List
import qualified Agda.Utils.List1 as List1

import Agda.Utils.Impossible

-- | Role of a character in the file.

data LayerRole = Markup | Comment | Code
  deriving (Show, Eq)

-- | A sequence of characters in a file playing the same role.

data Layer = Layer
  { layerRole    :: LayerRole
  , interval     :: IntervalWithoutFile
  , layerContent :: String
  } deriving Show

-- | A list of contiguous layers.

type Layers = [Layer]

instance HasRangeWithoutFile Layer where
  getRangeWithoutFile = getRangeWithoutFile . interval

-- | Annotates a tokenized string with position information.

mkLayers :: PositionWithoutFile -> [(LayerRole, String)] -> Layers
mkLayers pos []            = emptyLiterate pos
mkLayers pos ((_,"") : xs) = mkLayers pos xs
                             -- Empty layers are ignored.
mkLayers pos ((ty,s) : xs) =
  Layer ty (Interval () pos next) s : mkLayers next xs
  where
  next = movePosByString pos s

unMkLayers :: Layers -> [(LayerRole, String)]
unMkLayers = map ((,) <$> layerRole <*> layerContent)

atomizeLayers :: Layers -> [(LayerRole, Char)]
atomizeLayers = (fmap <$> ((,) . fst) <*> snd) <=< unMkLayers

-- | Type of a literate preprocessor:
--   Invariants:
--
--   > f : Processor
--
--   proposition> f pos s /= []
--
--   proposition> f pos s >>= layerContent == s

type Processor = PositionWithoutFile -> String -> [Layer]

-- | List of valid extensions for literate Agda files, and their
--   corresponding preprocessors.
--
--   If you add new extensions, remember to update test/Utils.hs so
--   that test cases ending in the new extensions are found.
--
--   The 'Bool' parameter controls whether only @```agda@ blocks
--   are treated as Agda code in Markdown/Typst files.
--   When @True@, only blocks marked @```agda@ are considered code.
--   When @False@, both @```@ and @```agda@ blocks are code (default).

literateProcessors :: Bool -> [(String, (Processor, FileType))]
literateProcessors onlyAgdaBlocks =
  ((,) <$> (".lagda" ++) . fst <*> snd) <$>
    [ (""    , (literateTeX, TexFileType))
    , (".rst", (literateRsT, RstFileType))
    , (".tex", (literateTeX, TexFileType))
    , (".md",  (literateMd onlyAgdaBlocks,  MdFileType ))
    , (".org", (literateOrg, OrgFileType))
    , (".tree", (literateTree, TreeFileType))
    -- For now, treat typst as markdown because they use the same
    -- syntax for code blocks.
    , (".typ", (literateMd onlyAgdaBlocks,  TypstFileType))
    ]

-- | List of valid literate file extensions.
literateExts :: [String]
literateExts = fst <$> literateProcessors False

-- | Returns @True@ if the role corresponds to Agda code.

isCode :: LayerRole -> Bool
isCode Code    = True
isCode Markup  = False
isCode Comment = False

-- | Returns @True@ if the layer contains Agda code.

isCodeLayer :: Layer -> Bool
isCodeLayer = isCode . layerRole

-- | Blanks the non-code parts of a given file, preserving positions of
--   characters corresponding to code. This way, there is a direct
--   correspondence between source positions and positions in the
--   processed result.

illiterate :: [Layer] -> String
illiterate xs = concat
  [ (if isCode layerRole then id else bleach) layerContent
  | Layer{layerRole, layerContent} <- xs
  ]

-- | Replaces non-space characters in a string with spaces.

bleach :: String -> String
bleach = map $ \ c -> if isSpace c && c /= '\t' then c else ' '

-- | Check if a character is a blank character.

isBlank :: Char -> Bool
isBlank = (&&) <$> isSpace <*> (/= '\n')

-- | Returns a tuple consisting of the first line of the input, and the rest
--   of the input.

caseLine :: a -> (String -> String -> a) -> String -> a
caseLine a k = \case
  []   -> a
  x:xs -> k (List1.toList line) rest
    where
    (line, rest) = breakAfter1 (== '\n') x xs

-- | Canonical decomposition of an empty literate file.

emptyLiterate :: PositionWithoutFile -> [Layer]
emptyLiterate pos = [Layer Markup (Interval () pos pos) ""]

-- | Create a regular expression that:
--   - Must match the whole string
--   - Works across line boundaries

rex :: String -> Regex
rex s =
  makeRegexOpts blankCompOpt{newSyntax = True} blankExecOpt $
    "\\`" ++ s ++ "\\'"

-- | Preprocessor for literate TeX.

literateTeX :: Processor
literateTeX pos s = mkLayers pos (tex s)
  where
  tex :: String -> [(LayerRole, String)]
  tex = caseLine [] $ \ line rest ->
    case r_begin `matchM` line of
      Just (getAllTextSubmatches -> [_, pre, _, markup, whitespace]) ->
        (Comment, pre) : (Markup, markup) :
        (Code, whitespace) : code rest
      Just _  -> __IMPOSSIBLE__
      Nothing -> (Comment, line) : tex rest

  r_begin = rex "(([^\\%]|\\\\.)*)(\\\\begin\\{code\\}[^\n]*)(\n)?"

  code :: String -> [(LayerRole, String)]
  code = caseLine [] $ \ line rest ->
    case r_end `matchM` line of
      Just (getAllTextSubmatches -> [_, code, markup, post]) ->
        (Code, code) : (Markup, markup) : (Comment, post) : tex rest
      Just _  -> __IMPOSSIBLE__
      Nothing -> (Code, line) : code rest

  r_end = rex "([[:blank:]]*)(\\\\end\\{code\\})(.*)"

-- | Preprocessor for Markdown.
--
--   The 'Bool' parameter controls whether only @```agda@ blocks
--   are treated as Agda code.
--   When @True@, only blocks explicitly marked @```agda@ are considered code.
--   When @False@, both unmarked @```@ and @```agda@ blocks are code (default).

literateMd :: Bool -> Processor
literateMd onlyAgdaBlocks pos s = mkLayers pos $ md s
  where
  md :: String -> [(LayerRole, String)]
  md = caseLine [] $ \ line rest ->
    case md_begin `matchM` line of
      Just (getAllTextSubmatches -> [_, pre, markup, tag]) ->
        -- When onlyAgdaBlocks is True, only treat blocks with "agda" tag as code
        if onlyAgdaBlocks && null tag
        then (Comment, pre) : (Comment, markup) : code_other rest
        else (Comment, pre) : (Markup, markup) : code rest
      Just _  -> __IMPOSSIBLE__
      Nothing ->
        (Comment, line) :
        if md_begin_other `match` line
        then code_other rest
        else md rest

  md_begin       = rex "(.*)([[:space:]]*```(agda)?[[:space:]]*)"
  md_begin_other = rex "[[:space:]]*```[a-zA-Z0-9-]*[[:space:]]*"

  code :: String -> [(LayerRole, String)]
  code = caseLine [] $ \ line rest ->
    case md_end `matchM` line of
      Just (getAllTextSubmatches -> [_, markup]) ->
        (Markup, markup) : md rest
      Just _  -> __IMPOSSIBLE__
      Nothing -> (Code, line) : code rest

  -- A non-Agda code block.
  code_other :: String -> [(LayerRole, String)]
  code_other = caseLine [] $ \ line rest ->
    (Comment, line) :
    if md_end `match` line
    then md rest
    else code_other rest

  md_end = rex "([[:space:]]*```[[:space:]]*)"

-- | Preprocessor for reStructuredText.

literateRsT :: Processor
literateRsT pos s = mkLayers pos $ rst s
  where
  rst :: String -> [(LayerRole, String)]
  rst = caseLine [] maybe_code

  maybe_code line rest =
    if r_comment `match` line then
      not_code
    else case r_code `match` line of
      []                         -> not_code
      [[_, before, "::", after]] ->
        -- Code starts
        if maybe True isBlank $ lastMaybe before then
          (Markup, line) : code rest
        else
          (Comment, before ++ ":") : (Markup, ":" ++ after) : code rest
      _ -> __IMPOSSIBLE__
    where
    not_code     = (Comment, line) : rst rest

  -- Finds the next indented block in the input.
  code :: String -> [(LayerRole, String)]
  code = caseLine [] $ \ line rest ->
    if all isSpace line then
      (Markup, line) : code rest
    else
      let xs = takeWhile isBlank line in
      if null xs
      then maybe_code line rest
      else (Code, line) : indented xs rest

  -- Process an indented block.
  indented :: String -> String -> [(LayerRole, String)]
  indented ind = caseLine [] $ \ line rest ->
    if all isSpace line || (ind `isPrefixOf` line)
          then (Code, line) : indented ind rest
          else maybe_code line rest

  -- Beginning of a code block.
  r_code = rex "(.*)(::)([[:space:]]*)"

  -- Beginning of a comment block.
  r_comment = rex "[[:space:]]*\\.\\.([[:space:]].*)?"

-- | Preprocessor for Org mode documents.

literateOrg :: Processor
literateOrg pos s = mkLayers pos $ org s
  where
  org :: String -> [(LayerRole, String)]
  org = caseLine [] $ \ line rest ->
    if org_begin `match` line then
      (Markup, line) : code rest
    else
      (Comment, line) : org rest

  -- Valid: #+begin_src agda2 :tangle yes
  -- Valid: #+begin_src agda2
  -- Invalid: #+begin_src adga2-foo
  org_begin = rex' "\\`(.*)([[:space:]]*\\#\\+begin_src agda2[[:space:]]+)"

  code :: String -> [(LayerRole, String)]
  code = caseLine [] $ \ line rest ->
    if org_end `match` line then
      (Markup, line) : org rest
    else
      (Code, line) : code rest

  org_end = rex' "\\`([[:space:]]*\\#\\+end_src[[:space:]]*)(.*)"

  -- Explicit type annotation required to disambiguate source.
  rex' :: String -> Regex
  -- Source blocks start with `#+begin_src` but the casing does not matter.
  rex' = makeRegexOpts blankCompOpt{newSyntax = True, caseSensitive = False} blankExecOpt

-- | Preprocessor for Forester documents

literateTree :: Processor
literateTree pos s = mkLayers pos (tree s)
  where
  tree :: String -> [(LayerRole, String)]
  tree = caseLine [] $ \ line rest ->
    case tree_begin `matchM` line of
      Just (getAllTextSubmatches -> [_, pre, _, markup, whitespace]) ->
        (Comment, pre) : (Markup, markup) :
        (Code, whitespace) : code rest
      Just _  -> __IMPOSSIBLE__
      Nothing -> (Comment, line) : tree rest

  tree_begin = rex "(([^\\%]|\\\\.)*)(\\\\agda\\{[^\n]*)(\n)?"

  code :: String -> [(LayerRole, String)]
  code = caseLine [] $ \ line rest ->
    case tree_end `matchM` line of
      Just (getAllTextSubmatches -> [_, code, markup, post]) ->
        (Code, code) : (Markup, markup) : (Comment, post) : tree rest
      Just _  -> __IMPOSSIBLE__
      Nothing -> (Code, line) : code rest

  tree_end = rex "([[:blank:]]*)(\\})(.*)"