basic stuff is done
This commit is contained in:
parent
db24be34dd
commit
93bfd31abf
2 changed files with 123 additions and 108 deletions
156
app/Main.hs
156
app/Main.hs
|
@ -7,49 +7,50 @@
|
||||||
|
|
||||||
module Main where
|
module Main where
|
||||||
|
|
||||||
|
import Config
|
||||||
import Control.Monad (forM, when)
|
import Control.Monad (forM, when)
|
||||||
import Data.List (sortOn)
|
|
||||||
import Development.Shake (Action, Rules, (|%>), (~>), (%>))
|
|
||||||
import Development.Shake.FilePath ((</>))
|
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
|
import Data.List (sortOn)
|
||||||
import qualified Data.Ord as Ord
|
import qualified Data.Ord as Ord
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import Data.Time
|
||||||
|
import Development.Shake (Action, Rules, (%>), (|%>), (~>))
|
||||||
import qualified Development.Shake as Shake
|
import qualified Development.Shake as Shake
|
||||||
|
import Development.Shake.FilePath ((</>))
|
||||||
import qualified Development.Shake.FilePath as FP
|
import qualified Development.Shake.FilePath as FP
|
||||||
import Config
|
import Templates
|
||||||
import Types
|
import Types
|
||||||
import Utilities
|
import Utilities
|
||||||
import Templates
|
|
||||||
import Data.Time
|
|
||||||
-- target = thing we want
|
-- target = thing we want
|
||||||
-- Rule = pattern of thing being made + actions to produce the thing
|
-- Rule = pattern of thing being made + actions to produce the thing
|
||||||
-- Action = actions to produce a thing
|
-- Action = actions to produce a thing
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
Shake.shakeArgs Shake.shakeOptions $ do
|
Shake.shakeArgs Shake.shakeOptions $ do
|
||||||
Shake.withTargetDocs "Build the site" $
|
Shake.withTargetDocs "Build the site" $
|
||||||
"build" ~> buildSite
|
"build" ~> buildSite
|
||||||
Shake.withTargetDocs "Clean the built site" $
|
Shake.withTargetDocs "Clean the built site" $
|
||||||
"clean" ~> Shake.removeFilesAfter outputDir ["//*"]
|
"clean" ~> Shake.removeFilesAfter outputDir ["//*"]
|
||||||
Shake.withoutTargets buildRules
|
Shake.withoutTargets buildRules
|
||||||
|
|
||||||
buildSite :: Action ()
|
buildSite :: Action ()
|
||||||
buildSite = do
|
buildSite = do
|
||||||
-- static files
|
-- static files
|
||||||
assetPaths <- Shake.getDirectoryFiles "" assetGlobs
|
assetPaths <- Shake.getDirectoryFiles "" assetGlobs
|
||||||
-- path concat each asset path so it's output into the outputDir
|
-- path concat each asset path so it's output into the outputDir
|
||||||
Shake.need $ map (outputDir </>) assetPaths
|
Shake.need $ map (outputDir </>) assetPaths
|
||||||
|
|
||||||
-- take the misc pages which aren't blog posts and make their html files
|
|
||||||
Shake.need $ map indexHtmlOutputPath pagePaths
|
|
||||||
|
|
||||||
-- handle posts
|
-- take the misc pages which aren't blog posts and make their html files
|
||||||
postPaths <- Shake.getDirectoryFiles "" postGlobs
|
Shake.need $ map indexHtmlOutputPath pagePaths
|
||||||
Shake.need $ map indexHtmlOutputPath postPaths
|
|
||||||
|
|
||||||
-- remaining pages, index.xml = rss feed
|
-- handle posts
|
||||||
Shake.need $ map (outputDir </>) ["index.html", "index.xml"]
|
postPaths <- Shake.getDirectoryFiles "" postGlobs
|
||||||
|
Shake.need $ map indexHtmlOutputPath postPaths
|
||||||
|
|
||||||
|
-- remaining pages, index.xml = rss feed
|
||||||
|
Shake.need $ map (outputDir </>) ["index.html", "index.xml"]
|
||||||
|
|
||||||
buildRules :: Rules ()
|
buildRules :: Rules ()
|
||||||
buildRules = do
|
buildRules = do
|
||||||
|
@ -61,36 +62,43 @@ buildRules = do
|
||||||
|
|
||||||
-- make a rule of the pattern outputDir/asset_name which copes from outputDir/../pages
|
-- make a rule of the pattern outputDir/asset_name which copes from outputDir/../pages
|
||||||
assets :: Rules ()
|
assets :: Rules ()
|
||||||
assets = map (outputDir </>) assetGlobs |%> \target -> do
|
assets =
|
||||||
let src = FP.dropDirectory1 target
|
map (outputDir </>) assetGlobs |%> \target -> do
|
||||||
Shake.copyFileChanged src target
|
let src = FP.dropDirectory1 target
|
||||||
Shake.putInfo $ "Copied " <> target <> " from " <> src
|
Shake.copyFileChanged src target
|
||||||
|
Shake.putInfo $ "Copied " <> target <> " from " <> src
|
||||||
|
|
||||||
|
-- handling typst only because pages should only be typst no reason for backwards compat on that
|
||||||
pages :: Rules ()
|
pages :: Rules ()
|
||||||
pages = map indexHtmlOutputPath pagePaths |%> \target -> do
|
pages =
|
||||||
let src = indexHtmlTypstSourcePath target
|
map indexHtmlOutputPath pagePaths |%> \target -> do
|
||||||
let metaSrc = indexHtmlTypstMetaPath target
|
let src = indexHtmlTypstSourcePath target
|
||||||
html <- typstToHtml src
|
let metaSrc = indexHtmlTypstMetaPath target
|
||||||
meta <- yamlToPost metaSrc
|
html <- typstToHtml src
|
||||||
let page = Page (postTitle meta) html
|
meta <- yamlToPost metaSrc
|
||||||
applyTemplateAndWrite "default.html" page target
|
let page = Page (postTitle meta) html
|
||||||
Shake.putInfo $ "Built " <> target <> " from " <> src
|
applyTemplateAndWrite "default.html" page target
|
||||||
|
Shake.putInfo $ "Built " <> target <> " from " <> src
|
||||||
|
|
||||||
-- there's probably a better way of doing this that allows for the target's origin file extension to get passed in but for now we're doing brute force
|
-- there's probably a better way of doing this that allows for the target's origin file extension to get passed in but for now we're doing brute force
|
||||||
postsRule :: Rules ()
|
postsRule :: Rules ()
|
||||||
postsRule = map indexHtmlOutputPath postGlobs |%> \target -> do
|
postsRule =
|
||||||
|
map indexHtmlOutputPath postGlobs |%> \target -> do
|
||||||
let potentials = indexHtmlSourcePaths target
|
let potentials = indexHtmlSourcePaths target
|
||||||
Shake.forP potentials (\path->do
|
Shake.forP
|
||||||
exists <- Shake.doesFileExist path
|
potentials
|
||||||
when exists
|
( \path -> do
|
||||||
(case FP.takeExtension path of
|
exists <- Shake.doesFileExist path
|
||||||
|
when
|
||||||
|
exists
|
||||||
|
( case FP.takeExtension path of
|
||||||
".typ" -> typstPost path
|
".typ" -> typstPost path
|
||||||
".md" -> markdownPost path
|
".md" -> markdownPost path
|
||||||
_ -> error $ "invalid file extension for post " <> target)
|
_ -> error $ "invalid file extension for post " <> target
|
||||||
)
|
)
|
||||||
|
)
|
||||||
return ()
|
return ()
|
||||||
|
|
||||||
typstPost :: FP.FilePath -> Action ()
|
typstPost :: FP.FilePath -> Action ()
|
||||||
typstPost src = do
|
typstPost src = do
|
||||||
Shake.need [src]
|
Shake.need [src]
|
||||||
|
@ -103,7 +111,6 @@ typstPost src = do
|
||||||
applyTemplateAndWrite "default.html" page target
|
applyTemplateAndWrite "default.html" page target
|
||||||
Shake.putInfo $ "Built " <> target <> " from " <> src
|
Shake.putInfo $ "Built " <> target <> " from " <> src
|
||||||
|
|
||||||
|
|
||||||
markdownPost :: FP.FilePath -> Action ()
|
markdownPost :: FP.FilePath -> Action ()
|
||||||
markdownPost src = do
|
markdownPost src = do
|
||||||
Shake.need [src]
|
Shake.need [src]
|
||||||
|
@ -117,49 +124,52 @@ markdownPost src = do
|
||||||
Shake.putInfo $ "Built " <> target <> " from " <> src
|
Shake.putInfo $ "Built " <> target <> " from " <> src
|
||||||
|
|
||||||
home :: Rules ()
|
home :: Rules ()
|
||||||
home = outputDir </> "index.html" %> \target -> do
|
home =
|
||||||
postPaths <- Shake.getDirectoryFiles "" postGlobs
|
outputDir </> "index.html" %> \target -> do
|
||||||
posts <- take 3
|
postPaths <- Shake.getDirectoryFiles "" postGlobs
|
||||||
. sortOn (Ord.Down . postDate)
|
posts <-
|
||||||
<$> forM postPaths readPost
|
take 3
|
||||||
html <- applyTemplate "home.html" $ HM.singleton "posts" posts
|
. sortOn (Ord.Down . postDate)
|
||||||
|
<$> forM postPaths readPost
|
||||||
|
html <- applyTemplate "home.html" $ HM.singleton "posts" posts
|
||||||
|
|
||||||
let page = Page (T.pack "Home") html
|
let page = Page (T.pack "Home") html
|
||||||
applyTemplateAndWrite "default.html" page target
|
applyTemplateAndWrite "default.html" page target
|
||||||
Shake.putInfo $ "Built " <> target
|
Shake.putInfo $ "Built " <> target
|
||||||
|
|
||||||
rss :: Rules ()
|
rss :: Rules ()
|
||||||
rss = outputDir </> "index.xml" %> \target -> do
|
rss =
|
||||||
|
outputDir </> "index.xml" %> \target -> do
|
||||||
postPaths <- Shake.getDirectoryFiles "" postGlobs
|
postPaths <- Shake.getDirectoryFiles "" postGlobs
|
||||||
posts <- sortOn (Ord.Down . postDate) <$> forM postPaths readPost
|
posts <- sortOn (Ord.Down . postDate) <$> forM postPaths readPost
|
||||||
applyTemplateAndWrite "feed.xml" (HM.singleton "posts" posts) target
|
applyTemplateAndWrite "feed.xml" (HM.singleton "posts" posts) target
|
||||||
|
|
||||||
Shake.putInfo $ "Built " <> target
|
Shake.putInfo $ "Built " <> target
|
||||||
|
|
||||||
readPost :: FilePath -> Action Post
|
readPost :: FilePath -> Action Post
|
||||||
readPost postPath = do
|
readPost postPath = do
|
||||||
case FP.takeExtension postPath of
|
case FP.takeExtension postPath of
|
||||||
".typ" -> readTypstPost postPath
|
".typ" -> readTypstPost postPath
|
||||||
".md" -> readMarkdownPost postPath
|
".md" -> readMarkdownPost postPath
|
||||||
_ -> error $ "unknown file extension for file" <> postPath
|
_ -> error $ "unknown file extension for file" <> postPath
|
||||||
|
|
||||||
readTypstPost :: FilePath -> Action Post
|
readTypstPost :: FilePath -> Action Post
|
||||||
readTypstPost postPath = do
|
readTypstPost postPath = do
|
||||||
html <- typstToHtml postPath
|
html <- typstToHtml postPath
|
||||||
post <- yamlToPost $ typstMetaPath postPath
|
post <- yamlToPost $ typstMetaPath postPath
|
||||||
Shake.putInfo $ "Read " <> postPath
|
Shake.putInfo $ "Read " <> postPath
|
||||||
return $ post
|
return $
|
||||||
{
|
post
|
||||||
postContent = Just html,
|
{ postContent = Just html,
|
||||||
postLink = Just . T.pack $ "/" <> FP.dropExtension postPath <> "/"
|
postLink = Just . T.pack $ "/" <> FP.dropExtension postPath <> "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
readMarkdownPost :: FilePath -> Action Post
|
readMarkdownPost :: FilePath -> Action Post
|
||||||
readMarkdownPost postPath = do
|
readMarkdownPost postPath = do
|
||||||
(post, html) <- markdownToHtml postPath
|
(post, html) <- markdownToHtml postPath
|
||||||
Shake.putInfo $ "Read " <> postPath
|
Shake.putInfo $ "Read " <> postPath
|
||||||
return $ post
|
return $
|
||||||
{
|
post
|
||||||
postContent = Just html,
|
{ postContent = Just html,
|
||||||
postLink = Just . T.pack $ "/" <> FP.dropExtension postPath <> "/"
|
postLink = Just . T.pack $ "/" <> FP.dropExtension postPath <> "/"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +1,40 @@
|
||||||
module Utilities where
|
module Utilities where
|
||||||
|
|
||||||
import Data.Text (Text)
|
|
||||||
import Development.Shake.FilePath ((<.>), (</>))
|
|
||||||
import qualified Data.Text as T
|
|
||||||
import Data.Yaml.Aeson
|
|
||||||
import qualified Development.Shake as Shake
|
|
||||||
import qualified Development.Shake.FilePath as Shake
|
|
||||||
import qualified Text.Pandoc as Pandoc
|
|
||||||
import Config
|
import Config
|
||||||
import Development.Shake (Action)
|
import Data.Aeson (Result (Error, Success))
|
||||||
import Types
|
|
||||||
import Data.Maybe (fromMaybe)
|
|
||||||
import Data.Time
|
|
||||||
import Text.Pandoc (Block (Plain), Meta (..), MetaValue (..), Pandoc (..))
|
|
||||||
import Data.Aeson (Result(Success, Error))
|
|
||||||
import qualified Data.Aeson as A
|
import qualified Data.Aeson as A
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
|
import Data.Text (Text)
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import Data.Time
|
||||||
|
import Data.Yaml.Aeson
|
||||||
|
import Development.Shake (Action)
|
||||||
|
import qualified Development.Shake as Shake
|
||||||
|
import Development.Shake.FilePath ((<.>), (</>))
|
||||||
|
import qualified Development.Shake.FilePath as Shake
|
||||||
|
import Text.Pandoc (Block (Plain), Meta (..), MetaValue (..), Pandoc (..))
|
||||||
|
import qualified Text.Pandoc as Pandoc
|
||||||
|
import Types
|
||||||
|
|
||||||
indexHtmlOutputPath :: FilePath -> FilePath
|
indexHtmlOutputPath :: FilePath -> FilePath
|
||||||
indexHtmlOutputPath srcPath =
|
indexHtmlOutputPath srcPath =
|
||||||
outputDir </> Shake.dropExtension srcPath </> "index.html"
|
outputDir </> Shake.dropExtension srcPath </> "index.html"
|
||||||
|
|
||||||
|
|
||||||
-- were applicative shenanigans necessary? no
|
-- were applicative shenanigans necessary? no
|
||||||
-- but using them felt cool
|
-- but using them felt cool
|
||||||
indexHtmlSourcePaths :: FilePath -> [FilePath]
|
indexHtmlSourcePaths :: FilePath -> [FilePath]
|
||||||
indexHtmlSourcePaths path = [indexHtmlTypstSourcePath, indexHtmlMarkdownSourcePath] <*> [path]
|
indexHtmlSourcePaths path = [indexHtmlTypstSourcePath, indexHtmlMarkdownSourcePath] <*> [path]
|
||||||
|
|
||||||
indexHtmlTypstSourcePath :: FilePath -> FilePath
|
indexHtmlTypstSourcePath :: FilePath -> FilePath
|
||||||
indexHtmlTypstSourcePath =
|
indexHtmlTypstSourcePath =
|
||||||
Shake.dropDirectory1
|
Shake.dropDirectory1
|
||||||
. (<.> "typ")
|
. (<.> "typ")
|
||||||
. Shake.dropTrailingPathSeparator
|
. Shake.dropTrailingPathSeparator
|
||||||
. Shake.dropFileName
|
. Shake.dropFileName
|
||||||
|
|
||||||
indexHtmlMarkdownSourcePath :: FilePath -> FilePath
|
indexHtmlMarkdownSourcePath :: FilePath -> FilePath
|
||||||
indexHtmlMarkdownSourcePath =
|
indexHtmlMarkdownSourcePath =
|
||||||
Shake.dropDirectory1
|
Shake.dropDirectory1
|
||||||
. (<.> "md")
|
. (<.> "md")
|
||||||
. Shake.dropTrailingPathSeparator
|
. Shake.dropTrailingPathSeparator
|
||||||
. Shake.dropFileName
|
. Shake.dropFileName
|
||||||
|
@ -46,7 +45,6 @@ indexHtmlTypstMetaPath = typstMetaPath . indexHtmlTypstSourcePath
|
||||||
typstMetaPath :: FilePath -> FilePath
|
typstMetaPath :: FilePath -> FilePath
|
||||||
typstMetaPath typstPath = Shake.dropExtension typstPath <.> "yaml"
|
typstMetaPath typstPath = Shake.dropExtension typstPath <.> "yaml"
|
||||||
|
|
||||||
|
|
||||||
typstToHtml :: FilePath -> Action Text
|
typstToHtml :: FilePath -> Action Text
|
||||||
typstToHtml filePath = do
|
typstToHtml filePath = do
|
||||||
content <- Shake.readFile' filePath
|
content <- Shake.readFile' filePath
|
||||||
|
@ -59,18 +57,24 @@ typstToHtml filePath = do
|
||||||
Pandoc.def {Pandoc.readerExtensions = Pandoc.pandocExtensions}
|
Pandoc.def {Pandoc.readerExtensions = Pandoc.pandocExtensions}
|
||||||
writerOptions =
|
writerOptions =
|
||||||
Pandoc.def {Pandoc.writerExtensions = Pandoc.pandocExtensions}
|
Pandoc.def {Pandoc.writerExtensions = Pandoc.pandocExtensions}
|
||||||
markdownToHtml :: FromJSON a => FilePath -> Action (a, Text)
|
|
||||||
|
markdownToHtml :: (FromJSON a) => FilePath -> Action (a, Text)
|
||||||
markdownToHtml filePath = do
|
markdownToHtml filePath = do
|
||||||
content <- Shake.readFile' filePath
|
content <- Shake.readFile' filePath
|
||||||
Shake.quietly . Shake.traced "Markdown to HTML" $ do
|
Shake.quietly . Shake.traced "Markdown to HTML" $ do
|
||||||
pandoc@(Pandoc meta _) <-
|
pandoc@(Pandoc meta _) <-
|
||||||
runPandoc . Pandoc.readMarkdown readerOptions . T.pack $ content
|
runPandoc . Pandoc.readMarkdown readerOptions . T.pack $ content
|
||||||
|
-- WARNING markdown needs to have no whitespace before/after dashes
|
||||||
|
-- print meta
|
||||||
meta' <- fromMeta meta
|
meta' <- fromMeta meta
|
||||||
html <- runPandoc . Pandoc.writeHtml5String writerOptions $ pandoc
|
html <- runPandoc . Pandoc.writeHtml5String writerOptions $ pandoc
|
||||||
return (meta', html)
|
return (meta', html)
|
||||||
where
|
where
|
||||||
readerOptions =
|
readerOptions =
|
||||||
Pandoc.def {Pandoc.readerExtensions = Pandoc.pandocExtensions}
|
Pandoc.def
|
||||||
|
{ Pandoc.readerStandalone = True,
|
||||||
|
Pandoc.readerExtensions = Pandoc.enableExtension Pandoc.Ext_yaml_metadata_block Pandoc.pandocExtensions
|
||||||
|
}
|
||||||
writerOptions =
|
writerOptions =
|
||||||
Pandoc.def {Pandoc.writerExtensions = Pandoc.pandocExtensions}
|
Pandoc.def {Pandoc.writerExtensions = Pandoc.pandocExtensions}
|
||||||
fromMeta (Meta meta) =
|
fromMeta (Meta meta) =
|
||||||
|
@ -91,24 +95,25 @@ markdownToHtml filePath = do
|
||||||
|
|
||||||
runPandoc :: Pandoc.PandocIO b -> IO b
|
runPandoc :: Pandoc.PandocIO b -> IO b
|
||||||
runPandoc action =
|
runPandoc action =
|
||||||
Pandoc.runIO (Pandoc.setVerbosity Pandoc.ERROR >> action)
|
Pandoc.runIO (Pandoc.setVerbosity Pandoc.ERROR >> action)
|
||||||
>>= either (fail . show) return
|
>>= either (fail . show) return
|
||||||
|
|
||||||
yamlToPost :: FilePath -> Action Post
|
yamlToPost :: FilePath -> Action Post
|
||||||
yamlToPost path = do
|
yamlToPost path = do
|
||||||
post <- decodeFileThrow path
|
post <- decodeFileThrow path
|
||||||
let post' = dateTransform post
|
let post' = dateTransform post
|
||||||
return $ fromMaybe post post'
|
return $ fromMaybe post post'
|
||||||
where
|
where
|
||||||
dateTransform post@(Post{postDate}) = do
|
dateTransform post@(Post {postDate}) = do
|
||||||
postDate' <- postDate
|
postDate' <- postDate
|
||||||
let postDate'' = dateStrTransform $ T.unpack postDate'
|
let postDate'' = dateStrTransform $ T.unpack postDate'
|
||||||
Just post {
|
Just
|
||||||
postDate = postDate''
|
post
|
||||||
}
|
{ postDate = postDate''
|
||||||
dateStrTransform date = do
|
}
|
||||||
date' <- parseTimeM False defaultTimeLocale "%Y-%-m-%-d" date
|
dateStrTransform date = do
|
||||||
Just $ T.pack $ formatTime @UTCTime defaultTimeLocale "%b %e, %Y" date'
|
date' <- parseTimeM False defaultTimeLocale "%Y-%-m-%-d" date
|
||||||
|
Just $ T.pack $ formatTime @UTCTime defaultTimeLocale "%b %e, %Y" date'
|
||||||
|
|
||||||
isTypstPost :: FilePath -> Bool
|
isTypstPost :: FilePath -> Bool
|
||||||
isTypstPost path = Shake.takeExtension path == ".typ"
|
isTypstPost path = Shake.takeExtension path == ".typ"
|
||||||
|
|
Loading…
Reference in a new issue