packages feed

tldr-0.9.2: src/Tldr/Parser.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE BangPatterns #-}

module Tldr.Parser where

import Prelude              hiding (takeWhile)
import Control.Applicative
import Data.Attoparsec.Combinator
import Data.Attoparsec.Text
import Data.Text                   (Text)

import qualified Data.Text as T

-- $setup
-- >>> :set -XOverloadedStrings
-- >>> import Data.Attoparsec.Text


-- | Parses '{{foo}}' blocks in CommonMark Code, such that:
--
-- * `ls {{foo}} bar` -> `[Left "ls ", Right "foo", Left " bar"]`
--
-- >>> parseOnly codeParser ""
-- Right []
-- >>> parseOnly codeParser "tar"
-- Right [Left "tar"]
-- >>> parseOnly codeParser "tar{"
-- Right [Left "tar{"]
-- >>> parseOnly codeParser "tar{{"
-- Right [Left "tar{{"]
-- >>> parseOnly codeParser "tar{{{"
-- Right [Left "tar{{{"]
-- >>> parseOnly codeParser "tar}"
-- Right [Left "tar}"]
-- >>> parseOnly codeParser "tar{{{b}"
-- Right [Left "tar{{{b}"]
-- >>> parseOnly codeParser "tar{{{b}}"
-- Right [Left "tar",Right "{b"]
-- >>> parseOnly codeParser "tar{{b}}}"
-- Right [Left "tar",Right "b}"]
-- >>> parseOnly codeParser "tar xf {{source.tar[.gz|.bz2|.xz]}} --directory={{directory}}"
-- Right [Left "tar xf ",Right "source.tar[.gz|.bz2|.xz]",Left " --directory=",Right "directory"]
codeParser :: Parser [Either Text Text]
codeParser = collectEither <$> outer
 where
  inner :: Parser [Either Text Text]
  inner = do
    _ <- char '{'
    _ <- char '{'
    l <- takeWhile (/= '}')
    e <- optional findEnd
    case e of
      Just e' -> (\o -> [Right (l <> e')         ] <> o) <$> (outer <|> pure [])
      Nothing -> (\o -> [Left  (T.pack "{{" <> l)] <> o) <$> (outer <|> pure [])
   where
    findEnd :: Parser Text
    findEnd = do
      c1 <- anyChar
      (p2, p3) <- peek2Chars
      case (c1, p2, p3) of
        ('}', Just '}', Just '}') -> (T.singleton '}' <>) <$> findEnd
        ('}', Just '}', _)        -> mempty <$ anyChar
        _                         -> fail ("Couldn't find end: " <> show (c1, p2, p3))

  outer :: Parser [Either Text Text]
  outer = do
    o  <- takeWhile (/= '{')
    (p1, p2) <- peek2Chars
    case (p1, p2) of
      (Just '{', Just '{') -> (\i   -> [Left o                   ] <> i) <$> (inner <|> ((\t -> [Left t]) <$> takeText))
      (Just '{', _)        -> (\a b -> [Left (o <> T.singleton a)] <> b) <$> anyChar <*> outer
      _                    -> pure [Left o]


-- | Collect both Lefts and Rights, mappending them to zore or one item per connected sublist.
--
-- >>> collectEither []
-- []
-- >>> collectEither [Right "abc", Right "def", Left "x", Left "z", Right "end"]
-- [Right "abcdef",Left "xz",Right "end"]
-- >>> collectEither [Right "", Right "def", Left "x", Left "", Right ""]
-- [Right "def",Left "x"]
collectEither :: (Eq a, Eq b, Monoid a, Monoid b) => [Either a b] -> [Either a b]
collectEither = go Nothing
 where
  go Nothing  [] = []
  go (Just !x) []
    | x == Right mempty || x == Left mempty = []
    | otherwise                             = [x]
  go Nothing           (Left  b:br) = go (Just (Left  b))        br
  go Nothing           (Right b:br) = go (Just (Right b))        br
  go (Just (Left !a))  (Left  b:br) = go (Just (Left (a <> b)))  br
  go (Just (Right !a)) (Right b:br) = go (Just (Right (a <> b))) br
  go (Just !a) xs
    | a == Right mempty || a == Left mempty = go Nothing xs
    | otherwise                             = a:go Nothing xs


-- | Peek 2 characters, not consuming any input.
peek2Chars :: Parser (Maybe Char, Maybe Char)
peek2Chars = lookAhead ((,) <$> optional anyChar <*> optional anyChar)