blog/blog-meta/post-lists.hs
rhiannon morris 5a94aae932 make less inefficient
- do all filters in one go
- do all post lists in one run of the program
- only write files if they are changed
  (so make repeats less work)
- simplify pandoc command for meta pages
  (this might not actually make a difference)
2024-12-03 20:17:27 +01:00

142 lines
4.4 KiB
Haskell
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Data.ByteString.Lazy qualified as LazyBS
import Data.Function ((&))
import Data.List (sortBy)
import Data.Ord (comparing)
import Data.Text (Text)
import Data.Text qualified as Text
import Misc
import System.FilePath qualified as Path
import System.FilePath.Find qualified as Find
import YAML ((.:), (.:?), (.!=), (##=))
import YAML qualified
import System.Environment
import System.IO
import System.Exit
import Data.Set (Set)
import qualified Data.Set as Set
import System.FilePath
import Data.Foldable
-- | generates a yaml list for a list of posts; either for a given tag, or all
-- posts. the posts are in chronological order, newest first.
--
-- example (exact yaml layout not guaranteed; whatever @HsYAML@ feels
-- like doing):
--
-- @
-- ---
-- title: "posts tagged ATS"
-- posts:
-- - file: fib.html
-- title: fibonacci in maude and ats
-- date: mon 24 october 2022
-- date-rss: Mon, 24 Oct 2022 00:00:01 UT
-- tags: [computer, maude, ATS]
-- summary: fibonacci numbers in the languages maude and ATS.
-- - file: ats.html
-- title: a little ats program
-- date: fri 16 september 2022
-- date-rss: Fri, 16 Sep 2022 00:00:01 UT
-- tags: [computer, ATS, cool languages]
-- summary: >
-- a little program in a little-known, little-documented language
-- called ATS.
-- ...
-- @
main :: IO ()
main = do
Opts {..} <- getOptions
files <- Find.findL True (pure True) (Find.extension Find.==? ".md") indir
infos <- traverse (getInfo indir) files
for_ (allTags infos) \tag -> do
let title = "pages tagged " <> tag <> ""
let basename = "tag-" <> Text.unpack (makeSlug tag) <.> "md"
let tagged = filter (checkTag $ Just tag) infos
makeTagInfo tagged title basename outdir
makeTagInfo infos "all posts" "index.md" outdir
makeTagInfo :: [PostInfo] -> Text -> String -> FilePath -> IO ()
makeTagInfo infos title basename outdir = do
let content = makeContent title infos
let filename = outdir </> basename
writeIfDifferentBS filename content
makeContent :: Text -> [PostInfo] -> LazyBS.ByteString
makeContent title is' = "---\n" <> YAML.encode1 val <> "...\n" where
is = sortBy (flip $ comparing date) is'
val = YAML.obj [("title" ##= title), ("posts" ##= is)]
allTags :: Foldable t => t PostInfo -> Set Text
allTags = foldMap (Set.fromList . tags)
-- | whether a post has the given tag
checkTag :: Maybe Text -> PostInfo -> Bool
checkTag Nothing _ = True
checkTag (Just t) i = t `elem` tags i
-- | command line options
data Options =
Opts {
-- | first argument: directory containing the .md files
indir :: !FilePath,
-- | second argument: write output to file
outdir :: !FilePath
}
getOptions :: IO Options
getOptions = do
prog <- getProgName
args <- getArgs
case args of
[indir, outdir] -> pure $ Opts {..}
_ -> do
hPutStrLn stderr $
"usage: " <> prog <> " INDIR OUTDIR\n\
\ --- collect tags in posts in INDIR into yaml files in OUTDIR"
exitFailure
-- | the front matter info we care about
data PostInfo =
Info {
-- | the (input) file name, relative to 'dir' in options
file :: FilePath,
title :: Text, -- ^ post @title@
date :: IsoDate, -- ^ post @date@
dateStr :: Text, -- ^ post @date@, but as text
tags :: [Text], -- ^ post @tags@
summary :: Maybe Text -- ^ post @summary@ (optional)
}
-- | @getInfo dir file@: read the front matter of @dir/file@
--
-- the 'file' field is just the filename, with @dir@ removed
getInfo :: FilePath -> FilePath -> IO PostInfo
getInfo dir file = do
yaml <- YAML.readHeader file
let dirs = Path.splitPath dir
let file' = Path.joinPath $ drop (length dirs) $ Path.splitPath file
unwrap file $ YAML.parseEither $
yaml & YAML.withMap "title, date, tags, summary?" \m ->
Info <$> pure file'
<*> m .: "title"
<*> m .: "date"
<*> m .: "date"
<*> m .: "tags" .!= []
<*> m .:? "summary"
instance YAML.ToYAML PostInfo where
toYAML (Info {..}) = YAML.obj
["date" ##= showDate date,
"date-rss" ##= rssDate date,
"date-iso" ##= dateStr,
"title" ##= title,
"tags" ##= tags,
"file" ##= htmlFile file,
"summary" ##= summary]
-- | replace @.md@ (or whatever) extension with @.html@
htmlFile :: FilePath -> Text
htmlFile f = Text.pack $ Path.replaceExtension f "html"