From 9f9e955524c79676eeef1d07f14057a90bdc564d Mon Sep 17 00:00:00 2001 From: flupe Date: Sun, 25 Sep 2022 21:51:43 +0200 Subject: [PATCH] templates everywhere --- content/assets/theme.css | 103 +++++++++++++++++++++++++++--------- content/index.html | 17 ++++++ content/index.md | 26 +++++++--- content/publications.yaml | 2 +- escot.cabal | 2 + src/Main.hs | 103 ++++++++++++++++++------------------ src/Template.hs | 106 ++++++++++++++++++++++++++++++++++++++ upload.sh | 1 + 8 files changed, 274 insertions(+), 86 deletions(-) create mode 100644 content/index.html create mode 100755 src/Template.hs create mode 100755 upload.sh diff --git a/content/assets/theme.css b/content/assets/theme.css index 57a42fc..d600025 100755 --- a/content/assets/theme.css +++ b/content/assets/theme.css @@ -1,80 +1,133 @@ +:root { + --large-width: 1080px; + --small-width: 600px; +} + body { font-family: sans-serif; - font-size: 13px; - max-width: 800px; + font-size: 14px; + max-width: var(--large-width); margin: 0 auto; - padding: 1em; + padding: 2em; display: grid; box-sizing: border-box; justify-content: center; line-height: 1.54; min-height: 100vh; background: #dce0df; - grid-template: "a m" 1fr - "f f" / 240px 1fr; + grid-template: "a m e" 1fr + ". f f" / 180px 1fr 180px; gap: 2em; } +h1, h2, h3, nav a { + font-family: monospace; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.1rem; +} + +nav { + padding: .4em 0 0; +} + +nav ul { + top: 2em; + position: sticky; + list-style: none; + margin: 0; + padding: 0 +} + +@media (max-width: 840px) { + body { + grid-template: "a" "m" 1fr "f"; + } + nav {padding: 0} + nav ul { position: static; } +} + +nav a { + display: block; + float: right; + clear: right; + text-decoration: none; + opacity: .7; +} +nav a:hover {opacity: 1} + a { + color: inherit; + /* font-weight: 400; */ /* text-decoration-line: overline; */ text-decoration-thickness: 1px; - text-decoration-color: rgba(0, 0, 0, 0.2); + text-decoration-color: rgba(0, 0, 0, 0.4); transition: .5s text-decoration-color; } a:hover { - text-decoration-color: rgba(0, 0, 0, 0.5); + text-decoration-color: rgba(0, 0, 0, 0.8); } ul { padding-left: 1em; } +main *+h2 {margin-top: 2rem} +main {margin-bottom: 2rem;} +main h2:first-child { margin-top: 0 } + /* aside info */ -#summary img { - border-radius: 2px; - max-width: 100%; - display: block; -} -#summary figure { margin: 0 0 1em; } -#summary table td:first-child { +#contact + table {font-family: monospace} +#contact + table td:first-child { padding-right: 1em; - font-weight: 600; + text-transform: uppercase; + font-weight: 500; } /* pubs list */ -#publications ul { +.pubs ul { list-style: none; margin: 0; padding: 0 0 0 2em; position: relative; } -#publications ul::before { +.pubs ul::before { content: attr(data-year); position: absolute; + font-family: monospace; writing-mode: vertical-rl; text-orientation: upright; left: 0; - top: .4em; - line-height: 1em; + top: .35em; + line-height: .8em; opacity: .5; } -#publications p { margin: 0; } -#publications li+li {margin: 1em 0 0} -#publications .title a { +.pubs p { margin: 0; } +.pubs li+li {margin: 1em 0 0} +.pubs .title a { text-decoration: none; color: #000; font-style: italic; font-weight: 500; } -#publications .buttons a { +.pubs .buttons { + margin-top: .5em; +} +.pubs .buttons a { display: inline-block; text-decoration: none; - border: 1px solid #666; + border: 1px solid #777; font-size: .8em; padding: 0 .5em; border-radius: 4px; + transition: .5s border-color; } -#publications .buttons a+a { +.pubs .buttons a:hover { + border-color: #555; +} +.pubs .buttons a+a { margin: 0 0 0 .5em; } + +footer {grid-area: f} diff --git a/content/index.html b/content/index.html new file mode 100644 index 0000000..fac4a05 --- /dev/null +++ b/content/index.html @@ -0,0 +1,17 @@ + + + + + + + + + + Lucas Escot + + + +
{{ $body }}
+ + + diff --git a/content/index.md b/content/index.md index 64edf8a..4996759 100755 --- a/content/index.md +++ b/content/index.md @@ -1,13 +1,25 @@ -## Lucas Escot +## Lucas Escot {#me} As of 2022, I am a PhD student at [TU Delft](https://tudelft.nl), in the [Programming Languages Group](https://pl.ewi.tudelft.nl/), under the supervision of Jesper Cockx. My work revolves around generic programming -in dependently-typed languages. +in dependently-typed languages --- namely, [Agda](https://github.com/agda/agda). -## Misc +## Miscellaneous -I use to enjoy drawing, and you can find some of my works on -[acatalepsie.fr](https://acatalepsie.fr). With a group of friends we run the -[sbi.re](https://sbi.re) network, for which we self-host a bunch of services. -It is here that I host my [personal blog](https://sbi.re/~lucas) where I ramble on the most --- which is, not a lot --- in French. +On my spare time, I do a fair bit of drawing. Some of it may be found over at [acatalepsie.fr](https://acatalepsie.fr). +Along with a group of friends, I am part of the [sbi.re](https://sbi.re) network, +under which we self-host a bunch of services. + +## Contact/Links {#contact} + +------- --------------------------------------- +Mail [lucas@escot.me](mailto:lucas@escot.me) +GPG [lescot.gpg](/lescot.gpg) +GH [flupe](https://github.com/flupe) +SRHT [flupe](https://sr.ht/~flupe) +------- --------------------------------------- + +## Publications + +{{ $publications }} diff --git a/content/publications.yaml b/content/publications.yaml index b97f184..049e6a5 100644 --- a/content/publications.yaml +++ b/content/publications.yaml @@ -24,5 +24,5 @@ file: papers/agda2hs-haskell22.pdf doi: "10.1145/3546189.3549920" venue: - name: Haskell 2022 + name: Haskell Symposium 2022 url: https://www.haskell.org/haskell-symposium/2022/ diff --git a/escot.cabal b/escot.cabal index 96ab1fa..f9e618a 100755 --- a/escot.cabal +++ b/escot.cabal @@ -9,6 +9,7 @@ executable escot main-is: Main.hs hs-source-dirs: src other-modules: Config + , Template build-depends: base , filepath , achille @@ -29,6 +30,7 @@ executable escot , optparse-applicative , process , directory + , megaparsec default-extensions: BlockArguments , TupleSections , OverloadedStrings diff --git a/src/Main.hs b/src/Main.hs index 4587272..4a1a868 100755 --- a/src/Main.hs +++ b/src/Main.hs @@ -1,29 +1,36 @@ {-# LANGUAGE LambdaCase, TypeSynonymInstances, FlexibleInstances, MultiParamTypeClasses, OverloadedStrings #-} -{-# LANGUAGE DuplicateRecordFields #-} +{-# LANGUAGE DuplicateRecordFields, ImportQualifiedPost #-} module Main where -import GHC.Generics (Generic) -import Data.Aeson (FromJSON) -import Data.Binary (Binary) -import Control.Monad ((>=>)) -import System.FilePath -import Data.Text (Text) -import Data.List (intersperse) -import Lucid +import GHC.Generics (Generic) +import Data.Aeson (FromJSON) +import Data.Binary (Binary) +import Control.Monad ((>=>)) +import Data.Function ((&)) +import Data.Text (Text) +import Data.List (intersperse) +import Data.Maybe (fromJust) import Data.Yaml +import System.FilePath +import Lucid import Control.Applicative ((<|>)) +import Data.Aeson.KeyMap qualified as KeyMap +import Data.Text.Lazy qualified as LT +import Data.Functor +import Text.Pandoc.Options +import Text.Pandoc.Shared (isHeaderBlock) +import Text.Pandoc.Definition (Pandoc(Pandoc), Block(Header)) import Achille import Achille.Writable (Writable) -import qualified Achille.Writable as Writable +import Achille.Writable qualified as Writable import Achille.Internal.IO (AchilleIO) - import Achille.Task.Pandoc -import Data.Functor +import Template (Template, Context, parseTemplate) +import Template qualified import Config (config, ropts, wopts, SiteConfig(title)) -import Text.Pandoc.Options -- Bibliography info @@ -63,9 +70,11 @@ parseYaml p = do Left err -> fail (prettyPrintParseException err) Right ok -> return ok +render :: Template -> Context -> Text -> LT.Text +render template ctx body = + KeyMap.insert "body" (String body) ctx + & Template.render template --- compile markdown with custom options and extensions -compileMD = compilePandocWith def { readerExtensions = pandocExtensions } def -- writing Html to disk (efficiently) instance AchilleIO m => Writable m (Html ()) where @@ -73,23 +82,42 @@ instance AchilleIO m => Writable m (Html ()) where main = achille do + index <- readTemplate "index.html" + match_ "assets/*" $ copyFile match_ "static/*" $ copyFileAs (makeRelative "static/") match_ "papers/*" $ copyFile - summary <- matchFile "summary.md" compileMD pubs :: [Publication] <- matchFile "publications.yaml" parseYaml - watch summary $ watch pubs $ match_ "index.md" \src -> do - compileMD src - <&> renderIndex summary (renderPublications pubs) - >>= write (src -<.> "html") + watch pubs $ match_ "index.md" \src -> do + doc <- readPandocWith def {readerExtensions = pandocExtensions} src + + let Pandoc _ blocks = doc + let headers = filter isHeaderBlock blocks + + -- parsing rendered md as template + template :: Template <- renderPandoc doc <&> parseTemplate <&> fromJust + + let ctx = KeyMap.insert "nav" (String (LT.toStrict $ renderText $ renderNav headers)) $ + KeyMap.insert "publications" (String (LT.toStrict $ renderText $ renderPublications pubs)) KeyMap.empty + + write (src -<.> "html") $ render index ctx (LT.toStrict $ Template.render template ctx) + + where + renderNav :: [Block] -> Html () + renderNav = ul_ . foldMap (\(Header _ (id, _, _) _) -> li_ $ a_ [href_ $ "#" <> id] (toHtmlRaw id)) + + readTemplate :: FilePath -> Task IO Template + readTemplate src = readText src <&> parseTemplate >>= \case + Just t -> return t + Nothing -> fail $ "Could not parse template: " ++ src -- TODO: fix year renderPublications :: [Publication] -> Html () -renderPublications pubs = section_ [ id_ "publications" ] do - h2_ "Publications" +renderPublications pubs = section_ [class_ "pubs"] do ul_ [ data_ "year" "2022" ] $ foldMap renderPub pubs + where renderPub :: Publication -> Html () renderPub Publication {..} = li_ [id_ slug] do p_ [class_ "title"] $ a_ [href_ ("#" <> slug)] $ toHtmlRaw title @@ -111,34 +139,3 @@ renderPublications pubs = section_ [ id_ "publications" ] do renderAuthors [one] = one renderAuthors (last:others) = mconcat (intersperse ", " (reverse others)) <> " and " <> last - -renderIndex :: Text -> Html () -> Text -> Html () -renderIndex summary pubs body = - doctypehtml_ do - head_ do - meta_ [ name_ "viewport" - , content_ "width=device-width, initial-scale=1.0, user-scalable=yes" - ] - meta_ [ name_ "theme-color", content_ "#000000" ] - meta_ [ name_ "robots", content_ "index, follow" ] - meta_ [ charset_ "utf-8" ] - link_ [ rel_ "stylesheet", href_ "/assets/theme.css" ] - link_ [ rel_ "shortcut icon" - , type_ "image/svg" - , href_ "/assets/favicon.svg" - ] - -- meta_ [ property_ "og:title", content_ title ] - -- meta_ [ property_ "og:type", content_ "website" ] - -- meta_ [ property_ "og:image", content_ image ] - -- meta_ [ property_ "og:description", content_ description ] - title_ "Lucas Escot" - - body_ do - aside_ [id_ "summary"] $ toHtmlRaw summary - main_ do - section_ $ toHtmlRaw body - pubs - footer_ do - "2021 · " - a_ [ href_ "https://creativecommons.org/licenses/by-nc/2.0/" ] - "CC BY-NC 2.0" diff --git a/src/Template.hs b/src/Template.hs new file mode 100755 index 0000000..ca59d26 --- /dev/null +++ b/src/Template.hs @@ -0,0 +1,106 @@ +-- | Tiny templating engine +module Template where + +import Data.Text +import Data.Text.Lazy.Encoding (encodeUtf8) +import Data.ByteString.Lazy (ByteString) +import Data.Binary +import Data.Functor (void) +import Data.Void +import GHC.Generics +import Text.Megaparsec +import Text.Megaparsec.Char +import Data.Aeson (Value(..)) + +import qualified Data.Text.Lazy as LT +import qualified Data.Aeson as Aeson +import qualified Data.Aeson.KeyMap as KeyMap +import qualified Data.Aeson.Key as Key +-- import qualified Data.HashMap.Strict as HashMap + + +data Chunk + = Raw Text + | Var [Text] + | For Text [Text] Template + | If [Text] Template + deriving (Eq, Show, Generic, Binary) + +type Template = [Chunk] +type Context = Aeson.Object + +type Parser = Parsec Void Text + + +-- RENDERING + +render :: Template -> Context -> LT.Text +render chunks ctx = foldMap (renderChunk ctx) chunks + +lookupCtx :: [Text] -> Context -> Maybe Aeson.Value +lookupCtx keys = aux keys . Aeson.Object + where + aux :: [Text] -> Aeson.Value -> Maybe Aeson.Value + aux [] v = Just v + aux (k:ks) (Object h) = KeyMap.lookup (Key.fromText k) h >>= aux ks + aux _ _ = Nothing + +renderChunk :: Context -> Chunk -> LT.Text +renderChunk ctx (Raw t) = LT.fromStrict t +renderChunk ctx (Var ks) = + case Aeson.fromJSON <$> lookupCtx ks ctx of + Just (Aeson.Success v) -> v + _ -> mempty +renderChunk ctx (For kn ks c) = + case lookupCtx ks ctx of + Just (Aeson.Array arr) -> + foldMap (\v -> foldMap (renderChunk (KeyMap.insert (Key.fromText kn) v ctx)) c) arr + _ -> mempty +renderChunk ctx (If ks c) = + case lookupCtx ks ctx of + Just _ -> foldMap (renderChunk ctx) c + _ -> mempty + + +-- PARSING + +parseTemplate :: Text -> Maybe Template +parseTemplate = parseMaybe templateP + +templateP :: Parser Template +templateP = many chunkP + +chunkP :: Parser Chunk +chunkP = choice [ varP , forP , ifP , rawP ] + where + identP :: Parser Text + identP = pack <$> ((:) <$> letterChar <*> many alphaNumChar) + + keysP :: Parser [Text] + keysP = try $ "$" *> sepBy1 identP "." + + varP :: Parser Chunk + varP = try $ Var <$> between ldelim rdelim + (space *> keysP <* space) + + rawP :: Parser Chunk + rawP = Raw . pack <$> + someTill anySingle (eof <|> lookAhead ldelim) + + forP :: Parser Chunk + forP = try do + ldelim *> space *> string "for" *> space1 + key <- identP <* space1 <* "in" <* space1 + val <- keysP <* space <* rdelim + chunks <- manyTill chunkP (try (between ldelim rdelim (space *> "end" *> space))) + return $ For key val chunks + + ifP :: Parser Chunk + ifP = try do + ldelim *> space *> string "if" *> space1 + val <- keysP <* space <* rdelim + chunks <- manyTill chunkP (try (between ldelim rdelim (space *> "end" *> space))) + return $ If val chunks + + ldelim = void "{{" + rdelim = "}}" diff --git a/upload.sh b/upload.sh new file mode 100755 index 0000000..e596e1b --- /dev/null +++ b/upload.sh @@ -0,0 +1 @@ +rsync -e 'ssh -p 222' -avz _site/ lucas@sbi.re:/var/lib/www/lucas.escot.me