{-# LANGUAGE ScopedTypeVariables, PatternGuards, FlexibleContexts #-} module Text.CSL.Util ( safeRead , readNum , (<^>) , capitalize , camelize , uncamelize , isPunct , last' , init' , words' , trim , triml , trimr , parseBool , parseString , parseInt , mb , (.#?) , (.#:) , onBlocks , titlecase , unTitlecase , protectCase , splitStrWhen , proc , proc' , procM , query , betterThan , toRead , inlinesToString , headInline , lastInline , tailInline , initInline , tailFirstInlineStr , toCapital , mapHeadInline , tr' , findFile , (&=) , mapping' ) where import Data.Aeson import Data.Aeson.Types (Parser) import Data.Text (Text) import qualified Data.Text as T import Data.Char (toLower, toUpper, isLower, isUpper, isPunctuation) import qualified Data.Traversable import Text.Pandoc.Shared (safeRead, stringify) import Text.Pandoc.Walk (walk) import Text.Pandoc import Data.List.Split (wordsBy) import Control.Monad.State import Data.Generics ( Typeable, Data, everywhere, everywhereM, mkM, everywhere', everything, mkT, mkQ ) import System.FilePath import System.Directory (doesFileExist) import qualified Data.Yaml.Builder as Y import Data.Yaml.Builder (ToYaml(..), YamlBuilder) #ifdef TRACE import qualified Debug.Trace import Text.Show.Pretty (ppShow) #endif tr' :: Show a => String -> a -> a #ifdef TRACE tr' note' x = Debug.Trace.trace ("=== " ++ note' ++ "\n" ++ ppShow x ++ "\n") x #else tr' _ x = x #endif readNum :: String -> Int readNum s = case reads s of [(x,"")] -> x _ -> 0 -- | Conjoin strings, avoiding repeated punctuation. (<^>) :: String -> String -> String [] <^> sb = sb sa <^> [] = sa sa <^> (s:xs) | s `elem` puncts && last sa `elem` puncts = sa ++ xs where puncts = ";:,. " sa <^> sb = sa ++ sb capitalize :: String -> String capitalize [] = [] capitalize (c:cs) = toUpper c : cs isPunct :: Char -> Bool isPunct c = c `elem` ".;?!" camelize :: String -> String camelize ('-':y:ys) = toUpper y : camelize ys camelize ('_':y:ys) = toUpper y : camelize ys camelize (y:ys) = y : camelize ys camelize _ = [] uncamelize :: String -> String uncamelize = foldr g [] . f where g x xs = if isUpper x then '-' : toLower x : xs else x : xs f ( x:xs) = toLower x : xs f [] = [] last' :: [a] -> [a] last' [] = [] last' xs = [last xs] init' :: [a] -> [a] init' [] = [] init' xs = init xs -- | Like words, but doesn't break on nonbreaking spaces etc. words' :: String -> [String] words' = wordsBy (\c -> c == ' ' || c == '\t' || c == '\r' || c == '\n') -- | Remove leading and trailing space (including newlines) from string. trim :: String -> String trim = triml . trimr triml :: String -> String triml = dropWhile (`elem` " \r\n\t") trimr :: String -> String trimr = reverse . triml . reverse -- | Parse JSON Boolean or Number as Bool. parseBool :: Value -> Parser Bool parseBool (Bool b) = return b parseBool (Number n) = case fromJSON (Number n) of Success (0 :: Int) -> return False Success _ -> return True Error e -> fail $ "Could not read boolean: " ++ e parseBool _ = fail "Could not read boolean" -- | Parse JSON value as String. parseString :: Value -> Parser String parseString (String s) = return $ T.unpack s parseString (Number n) = case fromJSON (Number n) of Success (x :: Int) -> return $ show x Error _ -> case fromJSON (Number n) of Success (x :: Double) -> return $ show x Error e -> fail $ "Could not read string: " ++ e parseString (Bool b) = return $ map toLower $ show b parseString v@(Array _)= inlinesToString `fmap` parseJSON v parseString v = fail $ "Could not read as string: " ++ show v -- | Parse JSON value as Int. parseInt :: Value -> Parser Int parseInt (String s) = case safeRead (T.unpack s) of Just n -> return n Nothing -> fail "Could not read Int" parseInt (Number n) = case fromJSON (Number n) of Success (x :: Int) -> return x Error e -> fail $ "Could not read string: " ++ e parseInt _ = fail "Could not read string" mb :: Monad m => (b -> m a) -> (Maybe b -> m (Maybe a)) mb = Data.Traversable.mapM -- | Parse as a string (even if the value is a number). (.#?) :: Object -> Text -> Parser (Maybe String) x .#? y = (x .:? y) >>= mb parseString (.#:) :: Object -> Text -> Parser String x .#: y = (x .: y) >>= parseString onBlocks :: ([Inline] -> [Inline]) -> [Block] -> [Block] onBlocks f bs = walk f' bs where f' (Para ils) = Para (f ils) f' (Plain ils) = Plain (f ils) f' x = x hasLowercaseWord :: [Inline] -> Bool hasLowercaseWord = any startsWithLowercase . splitStrWhen isPunctuation where startsWithLowercase (Str (x:_)) = isLower x startsWithLowercase _ = False splitUpStr :: [Inline] -> [Inline] splitUpStr = splitStrWhen (\c -> isPunctuation c || c == '\160') unTitlecase :: [Inline] -> [Inline] unTitlecase zs = evalState (caseTransform untc zs) SentenceBoundary where untc w = do st <- get case (w, st) of (y, NoBoundary) -> return y (Str (x:xs), WordBoundary) | isUpper x -> return $ Str (toLower x : xs) (Str (x:xs), SentenceBoundary) | isLower x -> return $ Str (toUpper x : xs) (Span ("",[],[]) xs, _) | hasLowercaseWord xs -> return $ Span ("",["nocase"],[]) xs _ -> return w protectCase :: [Inline] -> [Inline] protectCase zs = evalState (caseTransform protect zs) SentenceBoundary where protect (Span ("",[],[]) xs) | hasLowercaseWord xs = do st <- get case st of NoBoundary -> return $ Span ("",[],[]) xs _ -> return $ Span ("",["nocase"],[]) xs protect x = return x titlecase :: [Inline] -> [Inline] titlecase zs = evalState (caseTransform tc zs) SentenceBoundary where tc (Str (x:xs)) = do st <- get return $ case st of WordBoundary -> if isShortWord (x:xs) then Str (x:xs) -- or? map toLower (x:xs) else Str (toUpper x : xs) SentenceBoundary -> Str (toUpper x : xs) _ -> Str (x:xs) tc (Span ("",["nocase"],[]) xs) = return $ Span ("",["nocase"],[]) xs tc x = return x isShortWord s = map toLower s `elem` ["a","an","and","as","at","but","by","c","ca","d","de" ,"down","et","for","from" ,"in","into","nor","of","on","onto","or","over","so" ,"the","till","to","up","van","von","via","with","yet"] data CaseTransformState = WordBoundary | SentenceBoundary | NoBoundary caseTransform :: (Inline -> State CaseTransformState Inline) -> [Inline] -> State CaseTransformState [Inline] caseTransform xform = fmap reverse . foldM go [] . splitUpStr where go acc Space = do modify (\st -> case st of SentenceBoundary -> SentenceBoundary _ -> case acc of (Str [x]:_) | x `elem` "?!:" -> SentenceBoundary _ -> WordBoundary) return $ Space : acc go acc LineBreak = do put WordBoundary return $ Space : acc go acc (Str [c]) | c `elem` "-\2013\2014\160" = do put WordBoundary return $ Str [c] : acc | isPunctuation c = do -- leave state unchanged return $ Str [c] : acc go acc (Str []) = return acc go acc (Str xs) = do res <- xform (Str xs) put NoBoundary return $ res : acc go acc (Span ("",classes,[]) xs) | null classes || classes == ["nocase"] = do res <- xform (Span ("",classes,[]) xs) put NoBoundary return $ res : acc go acc (Quoted qt xs) = (:acc) <$> (Quoted qt <$> caseTransform xform xs) go acc (Emph xs) = (:acc) <$> (Emph <$> caseTransform xform xs) go acc (Strong xs) = (:acc) <$> (Strong <$> caseTransform xform xs) go acc (Link xs t) = (:acc) <$> (Link <$> caseTransform xform xs <*> pure t) go acc (Image xs t) = (:acc) <$> (Link <$> caseTransform xform xs <*> pure t) go acc (Span attr xs) = (:acc) <$> (Span attr <$> caseTransform xform xs) go acc x = return $ x : acc splitStrWhen :: (Char -> Bool) -> [Inline] -> [Inline] splitStrWhen _ [] = [] splitStrWhen p (Str xs : ys) = go xs ++ splitStrWhen p ys where go [] = [] go s = case break p s of ([],[]) -> [] (zs,[]) -> [Str zs] ([],(w:ws)) -> Str [w] : go ws (zs,(w:ws)) -> Str zs : Str [w] : go ws splitStrWhen p (x : ys) = x : splitStrWhen p ys -- | A generic processing function. proc :: (Typeable a, Data b) => (a -> a) -> b -> b proc f = everywhere (mkT f) -- | A generic processing function: process a data structure in -- top-down manner. proc' :: (Typeable a, Data b) => (a -> a) -> b -> b proc' f = everywhere' (mkT f) -- | A generic monadic processing function. procM :: (Monad m, Typeable a, Data b) => (a -> m a) -> b -> m b procM f = everywhereM (mkM f) -- | A generic query function. query :: (Typeable a, Data b, Monoid m) => (a -> m) -> b -> m query f = everything mappend (mempty `mkQ` f) betterThan :: [a] -> [a] -> [a] betterThan [] b = b betterThan a _ = a toRead :: String -> String toRead [] = [] toRead (s:ss) = toUpper s : camel ss where camel x | '-':y:ys <- x = toUpper y : camel ys | '_':y:ys <- x = toUpper y : camel ys | y:ys <- x = y : camel ys | otherwise = [] inlinesToString :: [Inline] -> String inlinesToString = stringify headInline :: [Inline] -> String headInline = take 1 . stringify lastInline :: [Inline] -> String lastInline xs = case stringify xs of [] -> [] ys -> [last ys] initInline :: [Inline] -> [Inline] initInline [] = [] initInline (i:[]) | Str s <- i = return $ Str (init' s) | Emph is <- i = return $ Emph (initInline is) | Strong is <- i = return $ Strong (initInline is) | Superscript is <- i = return $ Superscript (initInline is) | Subscript is <- i = return $ Subscript (initInline is) | Quoted q is <- i = return $ Quoted q (initInline is) | SmallCaps is <- i = return $ SmallCaps (initInline is) | Strikeout is <- i = return $ Strikeout (initInline is) | Link is t <- i = return $ Link (initInline is) t | Span at is <- i = return $ Span at (initInline is) | otherwise = [] initInline (i:xs) = i : initInline xs tailInline :: [Inline] -> [Inline] tailInline (Space:xs) = xs tailInline xs = removeEmpty $ tailFirstInlineStr xs where removeEmpty = dropWhile (== Str "") tailFirstInlineStr :: [Inline] -> [Inline] tailFirstInlineStr = mapHeadInline (drop 1) toCapital :: [Inline] -> [Inline] toCapital ils@(Span (_,["nocase"],_) _:_) = ils toCapital ils = mapHeadInline capitalize ils mapHeadInline :: (String -> String) -> [Inline] -> [Inline] mapHeadInline _ [] = [] mapHeadInline f (i:xs) | Str [] <- i = mapHeadInline f xs | Str s <- i = Str (f s) : xs | Emph is <- i = Emph (mapHeadInline f is) : xs | Strong is <- i = Strong (mapHeadInline f is) : xs | Superscript is <- i = Superscript (mapHeadInline f is) : xs | Subscript is <- i = Subscript (mapHeadInline f is) : xs | Quoted q is <- i = Quoted q (mapHeadInline f is) : xs | SmallCaps is <- i = SmallCaps (mapHeadInline f is) : xs | Strikeout is <- i = Strikeout (mapHeadInline f is) : xs | Link is t <- i = Link (mapHeadInline f is) t : xs | Span at is <- i = Span at (mapHeadInline f is) : xs | otherwise = i : xs findFile :: [FilePath] -> FilePath -> IO (Maybe FilePath) findFile [] _ = return Nothing findFile (p:ps) f = do exists <- doesFileExist (p f) if exists then return $ Just (p f) else findFile ps f (&=) :: (ToYaml a, Monoid a, Eq a) => Text -> a -> [(Text, YamlBuilder)] -> [(Text, YamlBuilder)] x &= y = \acc -> if y == mempty then acc else (x Y..= y) : acc mapping' :: [[(Text, YamlBuilder)] -> [(Text, YamlBuilder)]] -> YamlBuilder mapping' = Y.mapping . foldr ($) []