From 402107a237a7039b0aa2028f6c21939e53c98dc4 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 15 Oct 2023 22:22:18 +0200 Subject: support tag metadata, make much everything nicer --- Tags.hs | 171 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 Tags.hs (limited to 'Tags.hs') diff --git a/Tags.hs b/Tags.hs new file mode 100644 index 0000000..15d4b89 --- /dev/null +++ b/Tags.hs @@ -0,0 +1,171 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Tags where + +import qualified Data.Aeson.Key as K +import qualified Data.Aeson.KeyMap as KM +import Data.Foldable (traverse_) +import Data.List (inits, nub) +import Data.List.Extra (groupSort) +import qualified Data.Map as M +import Data.String (fromString) +import qualified Data.Text as T +import qualified Data.Yaml as Y +import Lens.Micro +import Lens.Micro.Aeson +import Lens.Micro.Mtl +import System.FilePath ((), splitDirectories, takeFileName) + +import AesonUtils +import Types +import Utils + +-- | Split a tag string into a hierarchical tag elements. Corner slashes and +-- slash groups are removed. +splitTag :: String -> [String] +splitTag = dropWhile (all (== '/')) . splitDirectories + +-- | Load all tag metadata from a directory. +sourceTagMeta :: FilePath -> Site () +sourceTagMeta fp = do + tmf <- use tagMetaFile + let go p + | takeFileName p == tmf = sourceTagMetaFile (fp p) + | otherwise = pure () + sourcePaths fp go + +-- | Load a given tag metadata file. +sourceTagMetaFile :: FilePath -> Site () +sourceTagMetaFile fp = do + yml' <- + io $ do + putStrLn $ "# <- " ++ fp + Y.decodeFileEither fp + case yml' of + Left err -> + error + ("Failed to load tag metadata from " ++ fp ++ ": " ++ + Y.prettyPrintParseException err) + Right yml -> traverse_ go (KM.toList yml) + where go :: (KM.Key, Y.Value) -> Site () + go (k, v') = + let ks = K.toString k + kx = splitTag ks + v + | Y.String t <- v' = + Y.Object $ KM.fromList [("title", Y.String t)] + | Y.Object _ <- v' = v' + | otherwise = + error ("invalid definition of tag " ++ ks ++ " in " ++ fp) + ins (Just ov) + | v == ov = Just ov + | otherwise = + error + ("conflicting tag metadata for tag " ++ ks ++ " in " ++ + fp) + ins Nothing = Just v + in tagMeta %= M.alter ins kx + +-- | Find a good display name for the _last_ hierarchical part of the htag. +getTagGroupName :: [String] -> Site String +getTagGroupName htag = + handleEmpty . maybe backup id . (>>= title) . (M.!? htag) <$> use tagMeta + where + title :: Y.Value -> Maybe String + title obj = obj ^? key "title" . _String . to T.unpack + backup + | null htag = "" + | null (last htag) = "(unnamed)" + | otherwise = last htag + handleEmpty x + | null x = "(root)" + | otherwise = x + +-- | Get all tags from the pages of the site and fill in the `htags` and +-- `ehtags` data. +sourceTags :: Site () +sourceTags = do + sgat <- + map + (second $ map splitTag . + (^.. pageMeta . key "tags" . values . _String . to T.unpack)) . + M.assocs <$> + use pages + ehtags .= M.fromList (invExpandTags sgat) + htags .= M.fromList (invTags sgat) + +-- | Organize a list of pages with hierarchical tags to a list with +-- hierarchical tags with pages attached; with tags implying parents. +invExpandTags :: [(FilePath, [[String]])] -> [([String], [FilePath])] +invExpandTags x = + map (fmap nub) $ groupSort [(t, p) | (p, htl) <- x, ht <- htl, t <- inits ht] + +-- | Like `invExpandTags` but without the expansion. +invTags :: [(FilePath, [[String]])] -> [([String], [FilePath])] +invTags x = map (fmap nub) $ groupSort [(t, p) | (p, htl) <- x, t <- htl] + +-- | Extract the known metadata about a given tag, or give null if there's none. +htagMeta :: [String] -> Site Y.Value +htagMeta htag = maybe Y.Null id . (M.!? htag) <$> use tagMeta + +-- | Make metadata for printing out a single hierarchical tag +htagRenderMeta :: ([String] -> Site FilePath) -> [String] -> Site Y.Value +htagRenderMeta makeLink htag = do + let htags = inits htag + links <- map fromString <$> traverse makeLink htags + names <- map fromString <$> traverse getTagGroupName htags + metas <- traverse htagMeta htags + pure $ + Y.object + [ ("href", last links) + , ("name", last names) + , ("meta", last metas) + , ( "htag" + , Y.array $ + zipWith3 + (\l n m -> Y.object [("href", l), ("name", n), ("meta", m)]) + links + names + metas) + ] + +-- | A generic helper for rendering metadata for tagged pages. +genericTaggedPagesRenderMeta :: + (FilePath -> Site Y.Value) + -> [String] + -> M.Map [String] [FilePath] + -> Site Y.Value +genericTaggedPagesRenderMeta makePageMeta htag tagmap = + Y.array <$> traverse makePageMeta (maybe [] id $ tagmap M.!? htag) -- TODO sort page listings here + +-- | Render metadata for all precisely tagged pages (not considering the +-- inheritance of tags following the hierarchy). +exactlyTaggedPagesRenderMeta :: + (FilePath -> Site Y.Value) -> [String] -> Site Y.Value +exactlyTaggedPagesRenderMeta makePageLinkMeta t = + use htags >>= genericTaggedPagesRenderMeta makePageLinkMeta t + +-- | Render metadata for all pages tagged by a given hierarchical tags (subtags +-- included). +allTaggedPagesRenderMeta :: + (FilePath -> Site Y.Value) -> [String] -> Site Y.Value +allTaggedPagesRenderMeta makePageLinkMeta t = + use ehtags >>= genericTaggedPagesRenderMeta makePageLinkMeta t + +-- | Like `htagRenderMeta`, but has hooks for extra metadata (e.g., listing of +-- pages) and for sub-tag rendering. That can be used for recursively building +-- metadata for whole tag hierarchies. +htagRenderMetaWithSubtags :: + ([String] -> Site FilePath) + -> ([String] -> Site Y.Value) + -> ([String] -> Site Y.Value) + -> [String] + -> Site Y.Value +htagRenderMetaWithSubtags makeLink extraMeta subtagMeta htag = do + meta <- htagRenderMeta makeLink htag + subtags <- + filter ((== htag) . init) . filter (not . null) . M.keys <$> use ehtags + {- TODO sort tag listings here -} + em <- extraMeta htag + subtagMetas <- Y.array . filter (/= Y.Null) <$> traverse subtagMeta subtags + pure $ meta `objMerge` Y.object [("subtags", subtagMetas)] `objMerge` em -- cgit v1.2.3