neuron-0.6.0.0: src/lib/Neuron/Zettelkasten/ID.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE NoImplicitPrelude #-}
module Neuron.Zettelkasten.ID
( ZettelID (..),
InvalidID (..),
zettelIDText,
parseZettelID,
parseZettelID',
idParser,
getZettelID,
zettelIDSourceFileName,
customIDParser,
)
where
import Data.Aeson
import Data.Aeson.Types (toJSONKeyText)
import qualified Data.Text as T
import Data.Time
import Neuron.Reader.Type (ZettelFormat, zettelFormatToExtension)
import Relude
import System.FilePath
import qualified Text.Megaparsec as M
import qualified Text.Megaparsec.Char as M
import Text.Megaparsec.Simple
import Text.Printf
import qualified Text.Show
data ZettelID
= -- | Short Zettel ID encoding `Day` and a numeric index (on that day).
ZettelDateID Day Int
| -- | Arbitrary alphanumeric ID.
ZettelCustomID Text
deriving (Eq, Show, Ord, Generic)
instance Show InvalidID where
show (InvalidIDParseError s) =
"Invalid Zettel ID: " <> toString s
instance FromJSON ZettelID where
parseJSON x = do
s <- parseJSON x
case parseZettelID' s of
Left e -> fail $ show e
Right zid -> pure zid
instance ToJSONKey ZettelID where
toJSONKey = toJSONKeyText zettelIDText
instance FromJSONKey ZettelID where
fromJSONKey = FromJSONKeyTextParser $ \s ->
case parseZettelID' s of
Right v -> pure v
Left e -> fail $ show e
instance ToJSON ZettelID where
toJSON = toJSON . zettelIDText
zettelIDText :: ZettelID -> Text
zettelIDText = \case
ZettelDateID day idx ->
formatDay day <> toText @String (printf "%02d" idx)
ZettelCustomID s -> s
formatDay :: Day -> Text
formatDay day =
subDay $ toText $ formatTime defaultTimeLocale "%y%W%a" day
where
subDay =
T.replace "Mon" "1"
. T.replace "Tue" "2"
. T.replace "Wed" "3"
. T.replace "Thu" "4"
. T.replace "Fri" "5"
. T.replace "Sat" "6"
. T.replace "Sun" "7"
zettelIDSourceFileName :: ZettelID -> ZettelFormat -> FilePath
zettelIDSourceFileName zid fmt = toString $ zettelIDText zid <> zettelFormatToExtension fmt
---------
-- Parser
---------
data InvalidID = InvalidIDParseError Text
deriving (Eq, Generic, ToJSON, FromJSON)
parseZettelID :: HasCallStack => Text -> ZettelID
parseZettelID =
either (error . show) id . parseZettelID'
parseZettelID' :: Text -> Either InvalidID ZettelID
parseZettelID' =
first InvalidIDParseError . parse idParser "parseZettelID"
idParser :: Parser ZettelID
idParser =
M.try (fmap (uncurry ZettelDateID) $ dayParser <* M.eof)
<|> fmap ZettelCustomID customIDParser
dayParser :: Parser (Day, Int)
dayParser = do
year <- parseNum 2
week <- parseNum 2
dayName <- dayFromIdx =<< parseNum 1
idx <- parseNum 2
day <-
parseTimeM False defaultTimeLocale "%y%W%a" $
printf "%02d" year <> printf "%02d" week <> dayName
pure (day, idx)
where
parseNum n = readNum =<< M.count n M.digitChar
readNum = maybe (fail "Not a number") pure . readMaybe
dayFromIdx :: MonadFail m => Int -> m String
dayFromIdx idx =
maybe (fail "Day should be a value from 1 to 7") pure $
["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] !!? (idx - 1)
customIDParser :: Parser Text
customIDParser = do
fmap toText $ M.some $ M.alphaNumChar <|> M.char '_' <|> M.char '-'
-- | Parse the ZettelID if the given filepath is a zettel.
getZettelID :: FilePath -> Maybe ZettelID
getZettelID fp =
let (name, _) = splitExtension $ takeFileName fp
in rightToMaybe $ parseZettelID' $ toText name