neuron-1.0.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 (..),
unsafeMkZettelID,
indexZid,
parseZettelID,
allowedSpecialChars,
idParser,
idParser',
getZettelID,
zettelIDSourceFileName,
)
where
import Data.Aeson
import Data.Aeson.Types (toJSONKeyText)
import qualified Data.Text as T
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 qualified Text.Show
data ZettelID = ZettelID
{ -- | Slug must be unique
zettelIDSlug :: Text,
-- | Actual ID used by the user, inside `[[..]]`
zettelIDRaw :: Text
}
deriving (Show, Ord, Generic)
-- | Make ZettelID from raw text.
--
-- Assumes that input text is already validated for allowed characters.
unsafeMkZettelID :: Text -> ZettelID
unsafeMkZettelID s =
let slug = T.intercalate "_" $ T.splitOn " " s
in ZettelID slug s
indexZid :: ZettelID
indexZid = unsafeMkZettelID "index"
instance Eq ZettelID where
(==) (ZettelID a _) (ZettelID b _) = a == b
instance Show InvalidID where
show (InvalidIDParseError s) =
"Invalid Zettel ID: " <> toString s
instance ToJSON ZettelID where
toJSON = toJSON . zettelIDRaw
instance FromJSON ZettelID where
parseJSON = fmap unsafeMkZettelID . parseJSON
instance ToJSONKey ZettelID where
toJSONKey = toJSONKeyText zettelIDRaw
instance FromJSONKey ZettelID where
fromJSONKey = FromJSONKeyTextParser $ \s ->
case parseZettelID s of
Right v -> pure v
Left e -> fail $ show e
zettelIDSourceFileName :: ZettelID -> ZettelFormat -> FilePath
zettelIDSourceFileName zid fmt =
toString (fn <> ext)
where
fn = zettelIDRaw zid
ext = zettelFormatToExtension fmt
---------
-- Parser
---------
data InvalidID = InvalidIDParseError Text
deriving (Eq, Generic, ToJSON, FromJSON)
parseZettelID :: Text -> Either InvalidID ZettelID
parseZettelID =
first InvalidIDParseError . parse idParser "parseZettelID"
-- | Characters, aside from alpha numeric characters, to allow in IDs
allowedSpecialChars :: [Char]
allowedSpecialChars =
[ '_',
'-',
'.',
-- Whitespace is essential for title IDs
' ',
-- Allow some puctuation letters that are common in note titles
',',
';',
'(',
')',
':',
'"',
'\''
]
idParser :: Parser ZettelID
idParser = idParser' allowedSpecialChars
idParser' :: String -> Parser ZettelID
idParser' cs = do
s <- M.some $ M.alphaNumChar <|> M.choice (M.char <$> cs)
pure $ unsafeMkZettelID (toText s)
-- | Parse the ZettelID if the given filepath is a zettel.
getZettelID :: ZettelFormat -> FilePath -> Maybe ZettelID
getZettelID fmt fp = do
let (name, ext) = splitExtension $ takeFileName fp
guard $ zettelFormatToExtension fmt == toText ext
rightToMaybe $ parseZettelID $ toText name