templates everywhere
This commit is contained in:
parent
7e6b53458b
commit
9f9e955524
|
@ -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}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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">
|
||||
<link rel="stylesheet" href="/assets/theme.css">
|
||||
<link rel="shortcut icon" type="image/svg" href="/assets/favicon.svg">
|
||||
<title>Lucas Escot</title>
|
||||
</head>
|
||||
<body>
|
||||
<nav>{{ $nav }}</nav>
|
||||
<main>{{ $body }}</main>
|
||||
<footer>2022 · <a href="https://creativecommons.org/licenses/by-nc/2.0/">CC BY-NC 2.0</a></footer>
|
||||
</body>
|
||||
</html>
|
|
@ -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 }}
|
||||
|
|
|
@ -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/
|
||||
|
|
|
@ -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
|
||||
|
|
91
src/Main.hs
91
src/Main.hs
|
@ -1,5 +1,5 @@
|
|||
{-# LANGUAGE LambdaCase, TypeSynonymInstances, FlexibleInstances, MultiParamTypeClasses, OverloadedStrings #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
{-# LANGUAGE DuplicateRecordFields, ImportQualifiedPost #-}
|
||||
|
||||
module Main where
|
||||
|
||||
|
@ -7,23 +7,30 @@ import GHC.Generics (Generic)
|
|||
import Data.Aeson (FromJSON)
|
||||
import Data.Binary (Binary)
|
||||
import Control.Monad ((>=>))
|
||||
import System.FilePath
|
||||
import Data.Function ((&))
|
||||
import Data.Text (Text)
|
||||
import Data.List (intersperse)
|
||||
import Lucid
|
||||
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"
|
||||
|
|
|
@ -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 = "}}"
|
Loading…
Reference in New Issue