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