clippings 0.1.3 → 0.2.0
raw patch · 12 files changed
+194/−161 lines, 12 filesdep +functor-infix
Dependencies added: functor-infix
Files
- clippings.cabal +13/−9
- examples/Clippings2TSV.hs +56/−0
- examples/clippings2tsv/Main.hs +0/−45
- src/Data/Functor/Extras.hs +0/−10
- src/Data/String/Extras.hs +0/−12
- src/Text/Kindle/Clippings/Reader.hs +75/−52
- src/Text/Kindle/Clippings/Types.hs +5/−10
- src/Text/Kindle/Clippings/Writer.hs +9/−9
- src/Text/Parsec/Combinator/Extras.hs +12/−10
- tests/Reader.hs +14/−4
- tests/fixtures/pw2clipping.txt +5/−0
- tests/fixtures/pw2pdfclipping.txt +5/−0
clippings.cabal view
@@ -1,10 +1,15 @@ name: clippings-version: 0.1.3+version: 0.2.0 license: MIT synopsis: A parser/generator for Kindle-format clipping files (`My Clippings.txt`), author: Vikram Verma <me@vikramverma.com> maintainer: Vikram Verma <me@vikramverma.com>-data-files: tests/fixtures/brackets.txt, tests/fixtures/clipping.txt, tests/fixtures/nested_brackets.txt+data-files:+ tests/fixtures/brackets.txt,+ tests/fixtures/clipping.txt,+ tests/fixtures/nested_brackets.txt,+ tests/fixtures/pw2clipping.txt,+ tests/fixtures/pw2pdfclipping.txt category: Text build-type: Simple cabal-version: >=1.10@@ -16,19 +21,17 @@ Text.Kindle.Clippings.Writer Text.Kindle.Clippings.Types other-modules:- Data.Functor.Extras,- Data.String.Extras, Text.Parsec.Combinator.Extras hs-source-dirs: src build-depends: base,- bifunctors, time, parsec, old-locale, strptime,- data-default+ data-default,+ functor-infix default-language: Haskell2010 ghc-options:@@ -59,9 +62,9 @@ executable clippings2tsv main-is:- Main.hs+ Clippings2TSV.hs hs-source-dirs:- examples/clippings2tsv+ examples/ build-depends: base == 4.*, bifunctors,@@ -69,7 +72,8 @@ cassava, parsec, bytestring,- safecopy+ safecopy,+ functor-infix default-language: Haskell2010 ghc-options:
+ examples/Clippings2TSV.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE RecordWildCards, LambdaCase #-}++module Main where++import Prelude hiding (putStr)+import Control.Applicative ((<$>))+import Data.Bifunctor (Bifunctor(bimap))+import Data.ByteString.Lazy (ByteString)+import Data.ByteString.Lazy.Char8 (putStr)+import Data.Char (ord)+import Data.Csv (ToRecord(..), EncodeOptions(..), encodeWith, defaultEncodeOptions, toField, record)+import Data.Functor.Infix ((<$$>))+import Data.Maybe (fromMaybe, catMaybes)+import Data.Monoid ((<>))+import System.Environment (getArgs)+import System.Exit (exitSuccess, exitFailure)+import System.IO (hPutStr, stderr)+import Text.Kindle.Clippings (Clipping(..), Document(..), Content(..), readClippings)+import Text.Parsec (parse)++data Card = Card { question :: String, answer :: String }++instance ToRecord Card where+ toRecord (Card q a) = record $ map toField [q,a]++class ToCard a where+ toCard :: a -> Maybe Card++instance ToCard Clipping where+ toCard c@Clipping{..} + | not (isHighlight c) = Nothing+ | null (show content) = Nothing+ | otherwise = Just $ Card content' author'+ where author' = fromMaybe "[clippings2tsv]" $ (" - " <>) <$> author document+ content' = \case { '\n' -> ' '; chr -> chr; } <$> show content++isHighlight :: Clipping -> Bool+isHighlight Clipping{..} = case content of+ Highlight _ -> True+ _ -> False++getClippings :: String -> Either String [Clipping]+getClippings = bimap show catMaybes + . parse readClippings [] ++encodeTabDelimited :: ToRecord a => [a] -> ByteString+encodeTabDelimited = encodeWith $ defaultEncodeOptions+ { encDelimiter = fromIntegral $ ord '\t' }++renderClippings :: [Clipping] -> ByteString+renderClippings = encodeTabDelimited . catMaybes . fmap toCard++main :: IO ()+main = head <$> getArgs >>= getClippings <$$> readFile >>= \case+ Left err -> hPutStr stderr err >> exitFailure+ Right cs -> putStr (renderClippings cs) >> exitSuccess
− examples/clippings2tsv/Main.hs
@@ -1,45 +0,0 @@-{-# LANGUAGE RecordWildCards, LambdaCase #-}--module Main where--import Prelude hiding (putStr)-import Control.Applicative ((<$>))-import Control.Applicative.Extras ((<$$>))-import Data.Bifunctor (Bifunctor(bimap))-import Data.ByteString.Lazy (ByteString)-import Data.ByteString.Lazy.Char8 (putStr)-import Data.Card (Card(..), ToCard(..))-import Data.Csv.Extras (encodeTabDelimited) -import Data.List.Extras (substitute)-import Data.Maybe (fromMaybe, catMaybes)-import Data.Monoid ((<>))-import System.Environment (getArgs)-import System.Exit (exitSuccess, exitFailure)-import System.IO (hPutStr, stderr)-import Text.Kindle.Clippings (Clipping(..), Document(..), Content(..), readClippings)-import Text.Parsec (parse)--instance ToCard Clipping where- toCard c@Clipping{..} - | not (isHighlight c) = Nothing- | null (show content) = Nothing- | otherwise = Just $ Card content' author'- where author' = fromMaybe "[clippings2tsv]" $ (" - " <>) <$> author document- content' = substitute '\n' ' ' $ show content--isHighlight :: Clipping -> Bool-isHighlight Clipping{..} = case content of- Highlight _ -> True- _ -> False--getClippings :: String -> Either String [Clipping]-getClippings = bimap show catMaybes - . parse readClippings [] --renderClippings :: [Clipping] -> ByteString-renderClippings = encodeTabDelimited . catMaybes . fmap toCard--main :: IO ()-main = head <$> getArgs >>= getClippings <$$> readFile >>= \case- Left err -> hPutStr stderr err >> exitFailure- Right cs -> putStr (renderClippings cs) >> exitSuccess
− src/Data/Functor/Extras.hs
@@ -1,10 +0,0 @@-module Data.Functor.Extras (- (<$$>),- for-) where--(<$$>) :: (Functor f, Functor g) => (a -> b) -> f (g a) -> f (g b)-(<$$>) = fmap fmap fmap--for :: Functor f => f a -> (a -> b) -> f b-for = flip fmap
− src/Data/String/Extras.hs
@@ -1,12 +0,0 @@-module Data.String.Extras (pad, chomp) where--import Data.Char (isSpace)--pad :: (String,String) -> (String,String)-pad (s0,s1) = (s0, pr++s1)- where pr = take (length s0 - length s1) s0--chomp :: String -> String-chomp = rstrip . lstrip- where lstrip = dropWhile isSpace- rstrip = reverse . lstrip . reverse
src/Text/Kindle/Clippings/Reader.hs view
@@ -1,89 +1,112 @@-module Text.Kindle.Clippings.Reader where+module Text.Kindle.Clippings.Reader+( readClipping+, readClippings+) where -import Control.Applicative ((<$>), (<*>), (*>), (<*), (<|>))-import Data.Bifunctor (bimap)-import Data.Functor.Extras ((<$$>),for)+import Control.Applicative ((<$>), (<*>), (*>), (<*), (<|>), liftA2)+import Control.Monad (join)+import Data.Functor.Infix ((<$$>)) import Data.List (find)-import Data.Maybe (fromJust, isJust)-import Data.String.Extras (pad, chomp)+import Data.Maybe (isJust, fromMaybe) import Data.Time.LocalTime (LocalTime) import Data.Time.Parse (strptime)-import Text.Kindle.Clippings.Types (Clipping(..),Location(..),Document(..),Position(..),Content(..))-import Text.Parsec (many1, digit, alphaNum, string, skipMany, oneOf, try, char, manyTill, anyToken)+import Text.Kindle.Clippings.Types (Clipping(..),Interval(..),Document(..),Position(..),Content(..))+import Text.Parsec (many1, digit, string, oneOf, try, char, manyTill, anyToken, lookAhead, many, noneOf, between) import Text.Parsec.String (Parser)-import Text.Parsec.Combinator.Extras (but, tryBut1, tryMaybe, tryString)+import Text.Parsec.Combinator.Extras (optional, but, tryString, stringCI) -eol :: Parser ()-eol = skipMany $ oneOf "\n\r"+data Tree = Leaf String | Node [Tree] -eor :: Parser String-eor = string "=========="+brackets :: Parser Tree+brackets = Node <$> between+ (char '(') (char ')')+ (many (brackets <|> (Leaf <$> many1 (noneOf "()")))) +instance Show Tree where+ show (Leaf x) = x+ show (Node xs) = "(" ++ concatMap show xs ++ ")"++node :: a -> ([Tree] -> a) -> (Tree -> a)+node _ fun (Node xs) = fun xs+node def _ _ = def++-- N.B.+-- The document parser (i.e. 'author' + 'title') is known to fail where+-- the author component includes unmatched parentheses, however this case+-- appears ambiguous in the grammar.++space :: Parser String+space = string " "++eol :: Parser String+eol = many1 $ oneOf "\n\r"+ readTitle :: Parser String-readTitle = chomp . concat <$> textAndBrackets- where brackets = (\a b c -> a:(concat b)++c) <$> char '(' <*> textAndBrackets <*> string ") "- textAndBrackets = many1 (tryBut1 "(\r\n)" <|> try brackets)+readTitle = manyTill anyToken (lookAhead . try $ space *> (try brackets <|> (Leaf <$> space)) *> eol) -readAuthor :: Parser (Maybe String)-readAuthor = tryMaybe $ init - <$> (char '(' *> but "\n\r")+readAuthor :: Parser String+readAuthor = node (error "The impossible happened!") (concatMap show) <$> brackets readContentType :: Parser String readContentType = (tryString "- Your " <|> string "- ") *> but " "- <* (tryString " on " <|> many1 (char ' '))+ <* (tryString " on " <|> tryString " at " <|> many1 (char ' ')) -readPageNumber :: Parser (Maybe Int)-readPageNumber = tryMaybe $ read - <$> (string "Page " *> many1 alphaNum <* string " | ")+parseSingletonInterval :: Parser Interval+parseSingletonInterval = Singleton . read <$> many1 digit -readLocation :: Parser (Maybe Location)-readLocation = tryMaybe - $ (tryString "Loc. " <|> string "Location ")- *> (try readLocationRegion <|> readLocationInt)- <* but "|" <* char '|' <* many1 (char ' ')+-- Early Kindle models sometimes described intervals of locations+-- with the prefix of the second part removed; e.g. ("1109", "12").+-- this padding will normalise this to (1109, 1112).+normaliseRegion :: String -> String -> (Int, Int)+normaliseRegion s0 s1 = (read s0, read $ take (length s0 - length s1) s0++s1) -readLocationInt :: Parser Location-readLocationInt = Location . read <$> many1 digit+parseProperInterval :: Parser Interval+parseProperInterval = (uncurry Proper <$$> normaliseRegion) <$> many1 digit <*> (char '-' *> many1 digit) -readLocationRegion :: Parser Location-readLocationRegion = parseRegion - <$$> (,) - <$> many1 digit - <*> (char '-' *> many1 digit)+parseInterval :: Parser Interval+parseInterval = try parseProperInterval <|> parseSingletonInterval -parseRegion :: (String, String) -> Location-parseRegion = Region . bimap read read . pad+readPage :: Parser Interval+readPage = stringCI "Page " *> parseInterval <* string " | " +readLocation :: Parser Interval+readLocation = (tryString "Loc. " <|> stringCI "Location ")+ *> parseInterval+ <* many1 (oneOf " |")+ parseDate :: String -> LocalTime-parseDate = fst . fromJust . fromJust {-[^1]-} . find isJust . for formats . flip strptime- where formats =- [ "%A, %d %B %y %X"- , "%A, %B %d, %Y %r"- , ""- ]--- [^1]: This is safe: `strptime x ""` is `Just` for all `x`.+parseDate raw = fromMaybe defaultLocalTime . join . find isJust . map (fst <$$> flip strptime raw) $+ [ "%A, %d %B %y %X" -- Thursday, 01 January 70 12:00:00 AM+ , "%A, %B %d, %Y %r" -- Thursday, January 01, 1970 12:00:00 AM+ ] +defaultLocalTime :: LocalTime+Just (defaultLocalTime, _) = strptime "" ""+ readDate :: Parser LocalTime-readDate = parseDate <$> (string "Added on " *> but "\n\r")+readDate = string "Added on " *> (parseDate <$> but "\n\r") +eor :: Parser String+eor = string "=========="+ readContent :: Parser String-readContent = chomp <$> manyTill anyToken (try eor)+readContent = manyTill anyToken (try $ many1 (oneOf "\n\r ") *> eor) readClipping :: Parser (Maybe Clipping) readClipping = clipping- <$> (Document <$> readTitle <*> readAuthor <* eol)+ <$> liftA2 Document readTitle (many1 space *> optional readAuthor) <* eol <*> readContentType- <*> (Position <$> readPageNumber <*> readLocation)+ <*> liftA2 Position (optional readPage) (optional readLocation) <*> readDate <* eol <*> readContent <* eol clipping :: Document -> String -> Position -> LocalTime -> String -> Maybe Clipping clipping d t p l c- |(==) t "Highlight" = Just $ Clipping d p l $ Highlight c- |(==) t "Note" = Just $ Clipping d p l $ Annotation c- |(==) t "Bookmark" = Just $ Clipping d p l Bookmark- |otherwise = Nothing+ | (==) t "Highlight" = Just . Clipping d p l $ Highlight c+ | (==) t "Note" = Just . Clipping d p l $ Annotation c+ | (==) t "Bookmark" = Just . Clipping d p l $ Bookmark+ | otherwise = Nothing readClippings :: Parser [Maybe Clipping] readClippings = many1 readClipping
src/Text/Kindle/Clippings/Types.hs view
@@ -2,7 +2,7 @@ ( Clipping (..) , Document (..) , Position (..)-, Location (..)+, Interval (..) , Content (..) ) where @@ -22,17 +22,12 @@ , author :: Maybe String -- Not always present, e.g. "Oxford Dictionary of English\n". } deriving (Eq) +data Interval = Singleton Int | Proper Int Int deriving (Eq)+ data Position = Position- { page :: Maybe Int- , location :: Maybe Location -- PDFs don't get these.+ { page :: Maybe Interval+ , location :: Maybe Interval } deriving (Eq)--data Location = Location Int | Region (Int,Int)--instance Eq Location where- Location a == Location b = a==b- Region a == Region b = a==b- _ == _ = False data Content = Bookmark | Highlight String | Annotation String
src/Text/Kindle/Clippings/Writer.hs view
@@ -6,26 +6,26 @@ import Data.Time.Format (formatTime) import Data.Time.LocalTime (LocalTime) import System.Locale (defaultTimeLocale)-import Text.Kindle.Clippings.Types (Clipping(..), Document(..), Position(..), Content(..), Location(..))+import Text.Kindle.Clippings.Types (Clipping(..), Document(..), Position(..), Content(..), Interval(..)) instance Show Document where show (Document t (Just a)) = t ++ " (" ++ a ++ ")" show (Document t Nothing) = t +instance Show Interval where+ show (Singleton i) = show i+ show (Proper i0 i1) = show i0 ++ "-" ++ show i1+ instance Show Position where- show (Position Nothing (Just l)) = show l- show (Position Nothing Nothing) = ""- show (Position (Just p) (Just l)) = "on Page " ++ show p ++ " | " ++ show l- show (Position (Just p) Nothing) = "on Page " ++ show p + show (Position Nothing (Just l)) = "Loc. " ++ show l+ show (Position (Just p) Nothing) = "on Page " ++ show p+ show (Position p@(Just _) l@(Just _)) = show (Position p Nothing) ++ " | " ++ show (Position Nothing l)+ show (Position Nothing Nothing) = "" instance Show Content where show (Highlight s) = s show (Annotation s) = s show (Bookmark) = ""--instance Show Location where- show (Location i) = "Loc. " ++ show i- show (Region (l1,l2)) = "Loc. " ++ show l1 ++ "-" ++ show l2 showContentType :: Content -> String showContentType (Highlight _) = "Highlight"
src/Text/Parsec/Combinator/Extras.hs view
@@ -1,21 +1,23 @@ module Text.Parsec.Combinator.Extras (+ optional, but,- tryBut1,- tryMaybe,- tryString+ tryString,+ stringCI ) where +import Control.Applicative ((<|>))+import Data.Char (toLower, toUpper) import Text.Parsec.String (Parser)-import Text.Parsec (many, many1, noneOf, try, optionMaybe, string)+import Text.Parsec (many, noneOf, try, optionMaybe, string, char) +optional :: Parser a -> Parser (Maybe a)+optional = optionMaybe . try+ but :: String -> Parser String but = many . noneOf -tryBut1 :: String -> Parser String-tryBut1 = try . many1 . noneOf--tryMaybe :: Parser a -> Parser (Maybe a)-tryMaybe = optionMaybe . try- tryString :: String -> Parser String tryString = try . string++stringCI :: String -> Parser String+stringCI = mapM (\c -> char (toUpper c) <|> char (toLower c))
tests/Reader.hs view
@@ -6,7 +6,7 @@ import Paths_clippings (getDataFileName) import Test.Assert (runAssertions) import Text.Kindle.Clippings.Reader (readClipping)-import Text.Kindle.Clippings.Types (Clipping(..),Location(..),Document(..),Position(..),Content(..))+import Text.Kindle.Clippings.Types (Clipping(..),Interval(..),Document(..),Position(..),Content(..)) import Text.Kindle.Clippings.Writer () import Text.Parsec (parse) @@ -23,7 +23,7 @@ inFixture = Clipping { date = LocalTime (fromGregorian 2013 06 10) (TimeOfDay 6 58 17) , document = Document "Haskell Monoids and their Uses" (Just "sigfpe")- , position = Position Nothing . Just $ Region (3,4)+ , position = Position Nothing . Just $ Proper 3 4 , content = Highlight "Haskell is a great language for constructing code modularly from small but orthogonal building blocks." } @@ -31,10 +31,18 @@ inPw2Fixture = Clipping { date = LocalTime (fromGregorian 2014 06 08) (TimeOfDay 20 36 53) , document = Document "Stand on Zanzibar" (Just "John Brunner")- , position = Position Nothing . Just $ Region (4607, 4607)+ , position = Position Nothing . Just $ Proper 4607 4607 , content = Highlight "Shinka will" } +inPw2pdfFixture :: Clipping+inPw2pdfFixture = Clipping+ { date = LocalTime (fromGregorian 2014 07 29) (TimeOfDay 07 53 28)+ , document = Document "Tyler Cowen-Creative Destruction_ How Globalization Is Changing the World's Cultures-Princeton University Press (2002)_k2opt" Nothing+ , position = Position (Just $ Proper 303 303) Nothing+ , content = Highlight "The fundamental story about consumer taste, in modern times, is not one of dumbing down or of producers seeking to satisfy a homogeneous least common denominator at the expense of quality. Rather, the basic trend is of increasing variety and diversity, at all levels of quality, high and low"+ }+ getTitle :: Clipping -> String getTitle = title . document @@ -43,15 +51,17 @@ main :: IO () main = do- [clipping, brackets, nested, pw2] <- mapM (readFile <=< getDataFileName)+ [clipping, brackets, nested, pw2, pw2pdf] <- mapM (readFile <=< getDataFileName) [ "tests/fixtures/clipping.txt" , "tests/fixtures/brackets.txt" , "tests/fixtures/nested_brackets.txt" , "tests/fixtures/pw2clipping.txt"+ , "tests/fixtures/pw2pdfclipping.txt" ] runAssertions $ [ ("Fixture should parse to sigfpe clipping.", getClipping clipping == inFixture) , ("Brackets in clippings' titles should be preserved." , getTitle (getClipping brackets) == "An Introduction to Statistical Learning: with Applications in R (Springer Texts in Statistics)") , ("Nested brackets in clippings' authors should be preserved.", getAuthor (getClipping nested) == Just "G. K. (Gilbert Keith) Chesterton") , ("Pw2Fixture should parse to Zanzibar clipping", getClipping pw2 == inPw2Fixture)+ , ("Pw2pdfFixture should parse to Tyler clipping", getClipping pw2pdf == inPw2pdfFixture) ]
+ tests/fixtures/pw2clipping.txt view
@@ -0,0 +1,5 @@+Stand on Zanzibar (John Brunner)+- Your Highlight on Location 4607-4607 | Added on Friday, June 8, 2014 8:36:53 PM++Shinka will+==========
+ tests/fixtures/pw2pdfclipping.txt view
@@ -0,0 +1,5 @@+Tyler Cowen-Creative Destruction_ How Globalization Is Changing the World's Cultures-Princeton University Press (2002)_k2opt +- Your Highlight on page 303-303 | Added on Tuesday, July 29, 2014 7:53:28 AM++The fundamental story about consumer taste, in modern times, is not one of dumbing down or of producers seeking to satisfy a homogeneous least common denominator at the expense of quality. Rather, the basic trend is of increasing variety and diversity, at all levels of quality, high and low+==========