packages feed

curly-expander-0.3.0.0: Text/CurlyExpander.hs

{-|
Module      : Text.CurlyExpander
License     : LGPL-3
Maintainer  : p-w@stty.cz
Stability   : testing
Portability : POSIX

This is the main (and only) module of the curly-expander package.

-}

{-# LANGUAGE OverloadedStrings #-}

module Text.CurlyExpander 
  (
    curlyExpand, 
    BackslashConfig (NoHandle, Preserve, Standard), 
    ExpandConfig (ExpandConfig, quotePairs, backslashConfig, persistQuotePairs, allowOneElementExpand), 
    defaultExpandConfig, 
    customCurlyExpand
  ) 
where

import qualified Data.Text as T
import qualified Data.Text.Lazy as L

import Data.Text.Lazy.Builder.Int (decimal)
import Data.Text.Lazy.Builder (toLazyText)

import Text.Parsec
import Text.Parsec.Text

import Data.Char

-- | This configuration specify, how should be backslashes handled.
-- It is part of `ExpandConfig`.
data BackslashConfig = 
    -- | If no handle is used, then backslashes are not handled in any special way.
    NoHandle | 

    -- | If preserve is used, backslashes are processed, any backslashed char is processed as nonspecial char 
    -- and backslashes aren't deleted from result.
    Preserve | 

    -- | If standard is used, backslashes are processed, any backslashed char is processed as nonspecial char
    -- and backslashes are deleted from result.
    Standard
  deriving Eq


-- | The curly braces expand config. 
-- It is used in `customCurlyExpand`.
data ExpandConfig = ExpandConfig {
    -- | The configuration, which defines, how should be backslashes handled (\\)
    backslashConfig :: BackslashConfig,

    -- | Quote pairs, which encloses a substrings, tells expander, that the substring shouldn't be expanded.
    -- For example (\"[\", \"]\") pairs tells to expander, that anything inside [ANYTHING] shouldn't be expanded.
    quotePairs :: [(String, String)],

    -- | If true, quote pairs aren't deleted. Otherwise they are deleted from a result.
    persistQuotePairs :: Bool,

    -- | If true, curly brackets around one element will be deleted. Otherwise they are persisted.
    allowOneElementExpand :: Bool
  }


-- | The default curly braces expand function config.
-- By default backslashes are not handeled, there are no quote pairs and one element expand is forbidden.
-- See the source code for details.
defaultExpandConfig :: ExpandConfig
defaultExpandConfig = ExpandConfig { 
    backslashConfig = NoHandle, 
    quotePairs = [],
    persistQuotePairs = False,
    allowOneElementExpand = False
  }

-- | Custom curly braces (brackets) expand function.
-- It works in the same way as curlyExpand, bud accept custom configuration `ExpandConfig` in the first argument.

customCurlyExpand :: ExpandConfig -> T.Text -> [T.Text]
customCurlyExpand config input =
  case parse inputP "bracket expansion"$ input of
    Left _ -> [input]
    Right ret -> map L.toStrict ret

  where 
    cumulatorComma :: Parser [L.Text]
    cumulatorComma = do
      atoms <- (try p_range) <|> (try p_char_range) <|> p_atoms
      return atoms 
      
      where
        p_range :: Parser [L.Text]
        p_range = do
          nb1 <- many1 digit
          _ <- string ".."
          nb2 <- many1 digit

          return$ map (toLazyText . decimal) $ get_range (read nb1) (read nb2)
          where
            get_range :: Int -> Int -> [Int]
            get_range n1 n2
              | n1 > n2 = reverse$ get_range n2 n1
              | otherwise = [n1..n2]

        p_char_range :: Parser [L.Text]
        p_char_range = do
          char1 <- anyChar
          _ <- string ".."
          char2 <- anyChar
          
          return [ L.pack [p] | p <- get_range char1 char2 ]
          where
            get_range :: Char -> Char -> [Char]
            get_range c1 c2
              | n1 > n2 = reverse$ get_range c2 c1
              | otherwise = map chr [n1..n2]

              where
                n1 = ord c1
                n2 = ord c2

        p_atoms :: Parser [L.Text]
        p_atoms = do
          molecule <- moleculeP
          terminal_atom <- innerInputP
          return $ (concat molecule) ++ terminal_atom

         where 
            moleculeP :: Parser [[L.Text]]
            moleculeP = 
             if allowOneElementExpand config; then
               many (try p_atom)
             else
               many1 (try p_atom)

        p_atom :: Parser [L.Text]
        p_atom = do

          atom <- innerInputP
          _ <- char ','
          return atom


    bracketP :: Parser [L.Text]
    bracketP = do

      _ <- char '{'
      ret <- cumulatorComma
      _ <- char '}'

      return$ ret

    charP :: Parser [L.Text]
    charP = do
      c <- anyChar
      return [L.pack [c]]

    nonSpecialCharP :: Parser [L.Text]
    nonSpecialCharP = do
      c <- noneOf ",}"
      return [L.pack [c]]

    backslashedP :: Parser [L.Text]
    backslashedP = do
      if handleBackslash then do
        _ <- char '\\'
        c <- anyChar

        return$ getReturnValue c
      else do
        unexpected "Char is not backslashed."

      where
        handleBackslash :: Bool
        handleBackslash = 
          if backslashConfig config == NoHandle then
            False
          else 
            True

        getReturnValue :: Char -> [L.Text]
        getReturnValue c =
          if backslashConfig config == Preserve then
            [ L.pack ['\\', c] ]
          else
            [ L.pack [c] ]

    specialQuotedP :: (String, String) -> Parser [L.Text]
    specialQuotedP (lQuote,rQuote) = do
      _ <- string lQuote
      ret <- quoteNext

      return$ [enrichReturnValue ret]
      where
        quoteClosure :: Parser L.Text
        quoteClosure = do
          _ <- string rQuote
          return ""

        quoteNextChar :: Parser L.Text
        quoteNextChar = do
          c <- anyChar
          rest <- quoteNext
          return$ L.pack [c] `L.append` rest

        quoteNext :: Parser L.Text
        quoteNext = (try quoteClosure <|> quoteNextChar)

        enrichReturnValue :: L.Text -> L.Text
        enrichReturnValue ret = 
          if persistQuotePairs config; then
            (L.pack lQuote) `L.append` ret `L.append` (L.pack rQuote) 
          else
            ret

        
    quotedP :: [(String, String)] -> Parser [L.Text]
    quotedP (quotes : rest) = (try$ specialQuotedP quotes) <|> quotedP rest
    quotedP [] = unexpected "String is not quoted."

    allQuotedP :: Parser [L.Text]
    allQuotedP = quotedP$ quotePairs config

    innerNonEmptyInputP :: Parser [L.Text]
    innerNonEmptyInputP = do
      molecule <- (backslashedP <|> try allQuotedP <|> try bracketP <|> nonSpecialCharP)
      rest <- innerInputP

      return [ L.append a b | a <- molecule, b <- rest ]

    innerInputP :: Parser [L.Text]
    innerInputP = (innerNonEmptyInputP <|> emptyInputP)

    nonEmptyInputP :: Parser [L.Text]
    nonEmptyInputP = do
      molecule <- (backslashedP <|> try allQuotedP <|> try bracketP <|> charP)
      rest <- inputP

      return [ L.append a b | a <- molecule, b <- rest ]

    emptyInputP :: Parser [L.Text]
    emptyInputP = do
      return [""]

    inputP :: Parser [L.Text]
    inputP = (nonEmptyInputP <|> emptyInputP)

-- | Curly braces (brackets) expand function
--
-- First argument is a `Data.Text`, which you want to expand. Second argument is a list of expanded `Data.Text`s.
--
-- There are given few usage examples:
--
-- >>> curlyExpand "car{A,B}"
-- ["carA","carB"]
--
-- >>> curlyExpand "car{1..5}"
-- ["car1","car2","car3","car4","car5"]
--
-- >>> curlyExpand "car{{A,B},{C,D}}"
-- ["carA", "carB", "carC", "carD"]
--
-- >>> curlyExpand "{car,bus}{A..C}"
-- ["carA", "carB", "carC", "busA", "busB", "busC"]
--
-- Be aware, that these examples will run only with `OverloadedStrings` language extension and proper `Data.Text` imports.

curlyExpand :: T.Text -> [T.Text]
curlyExpand input =
  customCurlyExpand defaultExpandConfig input