gdritter repos config-ini / master src / Data / Ini / Config / Raw.hs
master

Tree @master (Download .tar.gz)

Raw.hs @master

a7892ad
 
 
 
 
 
 
 
 
85ac32c
a7892ad
 
 
b729a0b
844e2db
 
1c5577d
12adbbe
 
b2e882b
b729a0b
 
1c13d05
 
 
 
844e2db
 
 
1c5577d
 
 
 
844e2db
 
1c5577d
 
49c9660
844e2db
49c9660
 
f614269
0e014d4
a7892ad
 
 
 
 
 
 
 
 
 
 
12adbbe
 
 
 
 
a7892ad
 
 
 
12adbbe
 
 
 
 
 
 
 
 
 
 
844e2db
b2e882b
 
 
b729a0b
 
1c5577d
844e2db
 
b2e882b
85ac32c
 
b2e882b
844e2db
 
b2e882b
 
12adbbe
b2e882b
 
 
 
844e2db
b2e882b
 
 
 
844e2db
b2e882b
 
 
 
1c5577d
b2e882b
 
 
844e2db
 
 
b2e882b
 
 
 
844e2db
1c5577d
b2e882b
 
 
 
 
1c5577d
b2e882b
1c5577d
b2e882b
1c5577d
 
 
 
 
295a994
844e2db
 
1c5577d
 
 
 
 
 
 
 
b2e882b
 
b729a0b
 
0e014d4
844e2db
 
b729a0b
d4e2a50
1c5577d
 
844e2db
1c5577d
 
844e2db
1c5577d
844e2db
2d0ff2d
0e014d4
1c5577d
 
b729a0b
1c5577d
b729a0b
844e2db
b729a0b
1c5577d
844e2db
 
2d0ff2d
844e2db
2ac2483
1c5577d
 
 
 
 
 
12adbbe
1c5577d
12adbbe
b729a0b
1c5577d
 
 
295a994
1c5577d
295a994
1c5577d
 
 
 
 
 
 
 
 
12adbbe
844e2db
12adbbe
1c5577d
844e2db
2d0ff2d
 
0e014d4
12adbbe
295a994
 
 
 
 
 
 
 
844e2db
 
0e014d4
1c5577d
 
b2e882b
 
 
b729a0b
 
1c5577d
 
 
 
 
 
 
 
 
 
 
 
 
 
295a994
1c5577d
 
b2e882b
60070c0
 
1c13d05
 
 
 
 
87208af
1c13d05
 
 
60070c0
1c13d05
 
60070c0
1c13d05
 
 
87208af
1c13d05
60070c0
1c13d05
87208af
1c13d05
 
60070c0
1c13d05
 
 
 
60070c0
1c13d05
87208af
{-|
Module     : Data.Ini.Config.Raw
Copyright  : (c) Getty Ritter, 2017
License    : BSD
Maintainer : Getty Ritter <config-ini@infinitenegativeutility.com>
Stability  : experimental

__Warning!__ This module is subject to change in the future, and therefore should
not be relied upon to have a consistent API.

-}
module Data.Ini.Config.Raw
( -- * INI types
  RawIni(..)
, IniSection(..)
, IniValue(..)
, BlankLine(..)
, NormalizedText(..)
, normalize
  -- * serializing and deserializing
, parseRawIni
, printRawIni
  -- * inspection
, lookupInSection
, lookupSection
, lookupValue
) where

import           Control.Monad (void)
import qualified Data.Foldable as F
import           Data.Monoid ((<>))
import           Data.Sequence (Seq)
import qualified Data.Sequence as Seq
import           Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as LazyText
import qualified Data.Text.Lazy.Builder as Builder
import           Data.Void (Void)
import           Text.Megaparsec
import           Text.Megaparsec.Char

type Parser = Parsec Void Text

-- | The 'NormalizedText' type is an abstract representation of text
-- which has had leading and trailing whitespace removed and been
-- normalized to lower-case, but from which we can still extract the
-- original, non-normalized version. This acts like the normalized
-- text for the purposes of 'Eq' and 'Ord' operations, so
--
-- @
--   'normalize' "  x  " == 'normalize' \"X\"
-- @
--
-- This type is used to store section and key names in the
data NormalizedText = NormalizedText
  { actualText     :: Text
  , normalizedText :: Text
  } deriving (Show)

-- | The constructor function to build a 'NormalizedText' value. You
-- probably shouldn't be using this module directly, but if for some
-- reason you are using it, then you should be using this function to
-- create 'NormalizedText' values.
normalize :: Text -> NormalizedText
normalize t = NormalizedText t (T.toLower (T.strip t))

instance Eq NormalizedText where
  NormalizedText _ x == NormalizedText _ y =
    x == y

instance Ord NormalizedText where
  NormalizedText _ x `compare` NormalizedText _ y =
    x `compare` y

-- | An 'Ini' value is a mapping from section names to
--   'IniSection' values. The section names in this mapping are
--   normalized to lower-case and stripped of whitespace. This
--   sequence retains the ordering of the original source file.
newtype RawIni = RawIni
  { fromRawIni :: Seq (NormalizedText, IniSection)
  } deriving (Eq, Show)

-- | An 'IniSection' consists of a name, a mapping of key-value pairs,
--   and metadata about where the section starts and ends in the
--   file. The section names found in 'isName' are __not__ normalized
--   to lower-case or stripped of whitespace, and thus should appear
--   exactly as they appear in the original source file.
data IniSection = IniSection
  { isName      :: Text
                   -- ^ The name of the section, as it appears in the
                   -- original INI source
  , isVals      :: Seq (NormalizedText, IniValue)
                   -- ^ The key-value mapping within that section. Key
                   -- names here are normalized to lower-case and
                   -- stripped of whitespace. This sequence retains
                   -- the ordering of the original source file.
  , isStartLine :: Int
                   -- ^ The line on which the section begins. This
                   -- field is ignored when serializing, and is only
                   -- used for error messages produced when parsing
                   -- and deserializing an INI structure.
  , isEndLine   :: Int
                   -- ^ The line on which the section ends. This field
                   -- is ignored when serializing, and is only used
                   -- for error messages produced when parsing and
                   -- deserializing an INI structure.
  , isComments  :: Seq BlankLine
                   -- ^ The blank lines and comments that appear prior
                   -- to the section head declaration, retained for
                   -- pretty-printing identical INI files.
  } deriving (Eq, Show)

-- | An 'IniValue' represents a key-value mapping, and also stores the
--   line number where it appears. The key names and values found in
--   'vName' and 'vValue' respectively are _not_ normalized to
--   lower-case or stripped of whitespace, and thus should appear
--   exactly as they appear in the original source file.
data IniValue = IniValue
  { vLineNo       :: Int
                     -- ^ The line on which the key/value mapping
                     -- appears. This field is ignored when
                     -- serializing, and is only used for error
                     -- messages produced when parsing and
                     -- deserializing an INI structure.
  , vName         :: Text
                     -- ^ The name of the key, as it appears in the INI source.
  , vValue        :: Text
                     -- ^ The value of the key
  , vComments     :: Seq BlankLine
  , vCommentedOut :: Bool
    -- ^ Right now, this will never show up in a parsed INI file, but
    --   it's used when emitting a default INI file: it causes the
    --   key-value line to include a leading comment as well.
  , vDelimiter    :: Char
  } deriving (Eq, Show)

-- | We want to keep track of the whitespace/comments in between KV
--   lines, so this allows us to track those lines in a reproducible
--   way.
data BlankLine
  = CommentLine Char Text
  | BlankLine
    deriving (Eq, Show)

-- | Parse a 'Text' value into an 'Ini' value, retaining a maximal
-- amount of structure as needed to reconstruct the original INI file.
parseRawIni :: Text -> Either String RawIni
parseRawIni t = case runParser pIni "ini file" t of
  Left err -> Left (errorBundlePretty err)
  Right v  -> Right v

pIni :: Parser RawIni
pIni = do
  leading <- sBlanks
  pSections leading Seq.empty

sBlanks :: Parser (Seq BlankLine)
sBlanks = Seq.fromList <$> many ((BlankLine <$ void eol) <|> sComment)

sComment :: Parser BlankLine
sComment = do
  c <- oneOf ";#"
  txt <- T.pack `fmap` manyTill anySingle eol
  return (CommentLine c txt)

pSections :: Seq BlankLine -> Seq (NormalizedText, IniSection) -> Parser RawIni
pSections leading prevs =
  pSection leading prevs <|> (RawIni prevs <$ void eof)

pSection :: Seq BlankLine -> Seq (NormalizedText, IniSection) -> Parser RawIni
pSection leading prevs = do
  start <- getCurrentLine
  void (char '[')
  name <- T.pack `fmap` some (noneOf "[]")
  void (char ']')
  void eol
  comments <- sBlanks
  pPairs (T.strip name) start leading prevs comments Seq.empty

pPairs :: Text
       -> Int
       -> Seq BlankLine
       -> Seq (NormalizedText, IniSection)
       -> Seq BlankLine
       -> Seq (NormalizedText, IniValue)
       -> Parser RawIni
pPairs name start leading prevs comments pairs = newPair <|> finishedSection
  where
    newPair = do
      (n, pair) <- pPair comments
      rs <- sBlanks
      pPairs name start leading prevs rs (pairs Seq.|> (n, pair))
    finishedSection = do
      end <- getCurrentLine
      let newSection = IniSection
            { isName      = name
            , isVals      = pairs
            , isStartLine = start
            , isEndLine   = end
            , isComments  = leading
            }
      pSections comments (prevs Seq.|> (normalize name, newSection))

pPair :: Seq BlankLine -> Parser (NormalizedText, IniValue)
pPair leading = do
  pos <- getCurrentLine
  key <- T.pack `fmap` some (noneOf "[]=:")
  delim <- oneOf ":="
  val <- T.pack `fmap` manyTill anySingle eol
  return ( normalize key
         , IniValue
             { vLineNo       = pos
             , vName         = key
             , vValue        = val
             , vComments     = leading
             , vCommentedOut = False
             , vDelimiter    = delim
             } )

getCurrentLine :: Parser Int
getCurrentLine = (fromIntegral . unPos . sourceLine) `fmap` getSourcePos


-- | Serialize an INI file to text, complete with any comments which
-- appear in the INI structure, and retaining the aesthetic details
-- which are present in the INI file.
printRawIni :: RawIni -> Text
printRawIni = LazyText.toStrict . Builder.toLazyText . F.foldMap build . fromRawIni
  where
    build (_, ini) =
      F.foldMap buildComment (isComments ini) <>
      Builder.singleton '[' <>
      Builder.fromText (isName ini) <>
      Builder.fromString "]\n" <>
      F.foldMap buildKV (isVals ini)
    buildComment BlankLine = Builder.singleton '\n'
    buildComment (CommentLine c txt) =
      Builder.singleton c <> Builder.fromText txt <> Builder.singleton '\n'
    buildKV (_, val) =
      F.foldMap buildComment (vComments val) <>
      (if vCommentedOut val then Builder.fromString "# " else mempty) <>
      Builder.fromText (vName val) <>
      Builder.singleton (vDelimiter val) <>
      Builder.fromText (vValue val) <>
      Builder.singleton '\n'

-- | Look up an Ini value by section name and key. Returns the sequence
-- of matches.
lookupInSection :: Text
                -- ^ The section name. Will be normalized prior to
                -- comparison.
                -> Text
                -- ^ The key. Will be normalized prior to comparison.
                -> RawIni
                -- ^ The Ini to search.
                -> Seq.Seq Text
lookupInSection sec opt ini =
    vValue <$> (F.asum (lookupValue opt <$> lookupSection sec ini))

-- | Look up an Ini section by name. Returns a sequence of all matching
-- section records.
lookupSection :: Text
              -- ^ The section name. Will be normalized prior to
              -- comparison.
              -> RawIni
              -- ^ The Ini to search.
              -> Seq.Seq IniSection
lookupSection name ini =
    snd <$> (Seq.filter ((== normalize name) . fst) $ fromRawIni ini)

-- | Look up an Ini key's value in a given section by the key. Returns
-- the sequence of matches.
lookupValue :: Text
            -- ^ The key. Will be normalized prior to comparison.
            -> IniSection
            -- ^ The section to search.
            -> Seq.Seq IniValue
lookupValue name section =
    snd <$> Seq.filter ((== normalize name) . fst) (isVals section)