packages feed

vimus-0.2.0: src/Vimus/Song/Format.hs

-- | Song format parser
module Vimus.Song.Format (
  SongFormat(..)
, parser

-- * exported for testing
, FormatTree(..)
, format
, meta
, alternatives
, parse
) where

import           Control.Applicative (Alternative(..), pure, liftA2)
import           Data.Default (Default(..))
import           Data.Foldable (asum)
import           Data.List (intercalate)
import           Data.Maybe (fromMaybe)
import           Data.Map (Map)
import qualified Data.Map as Map
import           Data.Monoid (Monoid(..))
import           Text.Printf (printf)

import qualified Network.MPD as MPD

import qualified Vimus.Command.Parser as Parser

import Vimus.Song


infixr 4 :+:

-- | AST for formats:
data FormatTree s m a =
    Empty
  | Pure a
  | FormatTree s m a :+: FormatTree s m a
  | Meta (s -> m a)
  | Alt [FormatTree s m a]

-- | Format AST
format
    :: (Alternative m, Monoid a)
    => a -- ^ default value for failed top-level metadata query
    -> s -- ^ container for metadata
    -> FormatTree s m a -> m a
format d n = top where
  top Empty     = empty
  top (Pure a)  = pure a
  top (x :+: y) = top x <#> top y
  top (Alt xs)  = asum $ fmap nested xs
  top (Meta f)  = f n <|> pure d -- if metadata query failed, replace failure with 'd'

  nested (Meta f)  = f n
  nested (x :+: y) = nested x <#> nested y
  nested t         = top t

  (<#>) = liftA2 mappend


newtype SongFormat = SongFormat (MPD.Song -> String)

instance Default SongFormat where
  def = SongFormat $ \song ->
    printf "%s - %s - %s - %s"
      (orNone $ artist song)
      (orNone $ album song)
      (orNone $ track song)
      (orNone $ title song)
   where
    orNone = fromMaybe "(none)"

parser :: Map String (MPD.Song -> Maybe String) -> Parser.Parser SongFormat
parser queries = Parser.Parser $ \str -> do
  tree <- parse queries str
  return (SongFormat (\song -> fromMaybe "(none)" $ format "none" song tree), "")

parse
  :: Map String (MPD.Song -> Maybe String)
  -> String
  -> Either Parser.ParseError (FormatTree MPD.Song Maybe String)
parse queries = go "" where
  go acc ('\\':'(':cs) = go ('(':acc) cs
  go acc ('\\':')':cs) = go (')':acc) cs
  go acc ('\\':'%':cs) = go ('%':acc) cs
  go acc ('(':cs) = do
    (xs, ys) <- alternatives cs
    alts     <- mapM (go "") xs
    rest     <- go "" ys
    return $ Pure (reverse acc) <+> Alt alts <+> rest
  go acc ('%':cs) = do
    (key, ys) <- meta cs
    case Map.lookup key queries of
      Nothing -> Left (Parser.ParseError $ "non-supported meta pattern: %" ++ key ++ "%")
      Just metadata -> do
        rest <- go "" ys
        return $ Pure (reverse acc) <+> Meta metadata <+> rest
  go acc (c:cs) = go (c:acc) cs
  go acc [] = Right (Pure (reverse acc))

  infixr 4 <+>
  Pure "" <+> x = x
  x <+> Pure "" = x
  x <+>       y = x :+: y


data Nat = Z | S Nat

-- | Parse alternatives pattern
alternatives :: String -> Either Parser.ParseError ([String], String)
alternatives = go Z [] "" where
  go n strings acc ('\\':'(':xs) = go n strings ('(':'\\':acc) xs
  go n strings acc ('\\':')':xs) = go n strings (')':'\\':acc) xs
  go n strings acc ('\\':'|':xs) = go n strings ('|':'\\':acc) xs
  go n strings acc ('(':xs)      = go (S n) strings ('(':acc) xs
  go Z strings acc (')':xs)      = Right (reverse (reverse acc : strings), xs)
  go (S n) strings acc (')':xs)  = go n strings (')':acc) xs
  go Z strings acc ('|':xs)      = go Z (reverse acc : strings) [] xs
  go n strings acc (x:xs)        = go n strings (x:acc) xs
  go _ strings acc []            = Left . Parser.ParseError $
    "unterminated alternatives pattern: (" ++ intercalate "|" (reverse acc : strings)

-- | Parse meta pattern
meta :: String -> Either Parser.ParseError (String, String)
meta = go "" where
  go acc ('\\':'%':xs) = go ('%':acc) xs
  go acc ('%':xs)      = Right (reverse acc, xs)
  go acc (x:xs)        = go (x:acc) xs
  go acc []            = Left (Parser.ParseError $ "unterminated meta pattern: %" ++ reverse acc)