commit 9eb5a038be652ebbeb9b4ec6d5430ec9b0ed2ee4 Author: gabriel-doriath-dohler Date: Tue Jun 13 23:59:59 2023 +0000 feat: static site generator diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..3550a30 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2904483 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +/.direnv + +# Nix build +result + +# Haskell +.stack-work +_cache +dist-newstyle +.ghc.environment.* diff --git a/README.md b/README.md new file mode 100644 index 0000000..ca537c2 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# dgnum.eu + +This is the website for dgnum.eu + +Enable nix flakes and install direnv. + +## Deploy + +TODO auto deploy from master using gitea hooks. + +## Build Static Site + +``` +nix build +``` + +## Writing Posts + +```console +direnv allow +cd site +ssg clean +ssg rebuild +ssg watch +``` + +## Working on the Static Site Generator + +```console +nix develop .#ssg +cd ssg +``` diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..8ea3b48 --- /dev/null +++ b/flake.lock @@ -0,0 +1,60 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1685518550, + "narHash": "sha256-o2d0KcvaXzTrPRIo0kOLV0/QXHhDQ5DTi+OxcjO8xqY=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "a1720a10a6cfe8234c0e93907ffe81be440f4cef", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1686501370, + "narHash": "sha256-G0WuM9fqTPRc2URKP9Lgi5nhZMqsfHGrdEbrLvAPJcg=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "75a5ebf473cd60148ba9aec0d219f72e5cf52519", + "type": "github" + }, + "original": { + "id": "nixpkgs", + "ref": "nixos-unstable", + "type": "indirect" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..81da4dc --- /dev/null +++ b/flake.nix @@ -0,0 +1,34 @@ +{ + description = "Website for dgnum.eu"; + + inputs = { + nixpkgs.url = "nixpkgs/nixos-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + ssg = pkgs.callPackage ./ssg { }; + defaultInputs = with pkgs; [ nixfmt shellcheck ]; + in { + formatter = pkgs.nixfmt; + + devShells = { + default = pkgs.mkShell { buildInputs = [ ssg ] ++ defaultInputs; }; + + ssg = pkgs.haskellPackages.shellFor { + packages = _: [ ssg ]; + buildInputs = with pkgs.haskellPackages; + [ cabal-install ghcid hlint ] ++ defaultInputs; + }; + }; + + packages = { + inherit ssg; + site = pkgs.callPackage ./site { inherit ssg; }; + }; + defaultPackage = self.packages.${system}.site; + }); +} diff --git a/ssg/LICENSE b/ssg/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/ssg/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/ssg/default.nix b/ssg/default.nix new file mode 100644 index 0000000..b524484 --- /dev/null +++ b/ssg/default.nix @@ -0,0 +1,4 @@ +{ haskellPackages, locale, nix-gitignore }: + +let srcs = nix-gitignore.gitignoreSourcePure ../.gitignore ./.; +in haskellPackages.callCabal2nix "ssg" srcs { } diff --git a/ssg/ssg.cabal b/ssg/ssg.cabal new file mode 100644 index 0000000..6619265 --- /dev/null +++ b/ssg/ssg.cabal @@ -0,0 +1,25 @@ +cabal-version: 3.0 +name: ssg +version: 0.1.0.0 +build-type: Simple + +license: Apache-2.0 +license-file: LICENSE +author: Gabriel Doriath Döhler +maintainer: gdd@dgnum.eu +copyright: Copyright 2023 Gabriel Doriath Döhler + +executable ssg + main-is: ssg.hs + build-depends: base ^>=4.16.4.0 + , blaze-html + , blaze-markup + , hakyll + , pandoc + , pandoc-types + ghc-options: -Wall + -threaded + -rtsopts + -with-rtsopts=-N + + default-language: Haskell2010 diff --git a/ssg/ssg.hs b/ssg/ssg.hs new file mode 100644 index 0000000..dad4e25 --- /dev/null +++ b/ssg/ssg.hs @@ -0,0 +1,272 @@ +-- Copyright 2023 Gabriel Doriath Döhler, Délégation Générale Numérique (DGNum) + +{-# LANGUAGE OverloadedStrings #-} + +import Control.Applicative ((<|>)) +import Control.Monad ((<=<), void) +import Data.Monoid (First(..)) +import Data.List (intersperse) + +import qualified Text.Blaze.Html5 as H +import qualified Text.Blaze.Html5.Attributes as A +import Text.Pandoc.Walk (query, walk) +import Text.Pandoc.Definition + ( Pandoc(..), Block(Header, Para, Plain), Inline(..) ) +import Text.Pandoc.Options ( WriterOptions(..), HTMLMathMethod( MathML ) ) +import Hakyll + +-------------------------------------------------------------------------------- + +nbPostsHome :: Int +nbPostsHome = 30 + +patternFrom :: [String] -> [String] -> Pattern +patternFrom basenames extensions = + foldl1 (.||.) $ map aux basenames + where + aux basename = + foldl1 (.||.) $ map (fromGlob . (++) (basename ++ "/**.")) extensions + +postPattern :: Pattern +postPattern = patternFrom [ "posts" ] [ "md" ] + +imgPattern :: Pattern +imgPattern = patternFrom [ ".", "images" ] [ "png", "jpeg", "jpg", "gif", "svg", "ico" ] + +docPattern :: Pattern +docPattern = patternFrom [ ".", "docs" ] [ "pdf", "ps", "djvu", "tex", "txt" ] + .||. patternFrom [ "docs" ] [ "md" ] + +miscPattern :: Pattern +miscPattern = foldl1 (.||.) [ "about.md", "services.md", "FAQ.md", "contact.md" ] + +-------------------------------------------------------------------------------- + +main :: IO () +main = hakyllWith config $ do + tags <- extractTags + authors <- extractAuthors + poles <- extractPoles + + tagsRules tags $ \tagStr tagsPattern -> do + let title = "Articles appartenant à la catégorie \"" ++ tagStr ++ "\"" + route idRoute + compile $ do + posts <- loadAll tagsPattern >>= recentFirst + let ctx = + constField "title" title + <> listField "posts" postCtx (return posts) + <> context + + makeItem "" + >>= loadAndApplyTemplate "templates/tag.html" ctx + >>= loadAndApplyTemplate "templates/default.html" ctx + >>= relativizeUrls + + tagsRules authors $ \authorStr authorsPattern -> do + let title = "Articles (co-)écrit par \"" ++ authorStr ++ "\"" + route idRoute + compile $ do + posts <- loadAll authorsPattern >>= recentFirst + let ctx = + constField "title" title + <> listField "posts" postCtx (return posts) + <> context + + makeItem "" + >>= loadAndApplyTemplate "templates/author.html" ctx + >>= loadAndApplyTemplate "templates/default.html" ctx + >>= relativizeUrls + + tagsRules poles $ \poleStr polesPattern -> do + let title = "Articles du pôle \"" ++ poleStr ++ "\"" + route idRoute + compile $ do + posts <- loadAll polesPattern >>= recentFirst + let ctx = + constField "title" title + <> listField "posts" postCtx (return posts) + <> context + + makeItem "" + >>= loadAndApplyTemplate "templates/pole.html" ctx + >>= loadAndApplyTemplate "templates/default.html" ctx + >>= relativizeUrls + + create [ "tags.html" ] $ do + route idRoute + compile $ renderTagList tags >>= tagCompiler + + create [ "authors.html" ] $ do + route idRoute + compile $ renderTagList authors >>= authorCompiler + + match (imgPattern .||. docPattern) $ do + route idRoute + compile copyFileCompiler + + match "css/*.css" $ do + route idRoute + compile compressCssCompiler + + match miscPattern $ do + route $ setExtension "html" + compile $ customPandocCompiler + >>= loadAndApplyTemplate "templates/default.html" context + >>= relativizeUrls + + match postPattern $ do + route $ setExtension "html" + compile $ customPandocCompiler + >>= loadAndApplyTemplate "templates/post.html" (postCtxWith tags authors poles) + >>= loadAndApplyTemplate "templates/default.html" (postCtxWith tags authors poles) + >>= relativizeUrls + + match (postPattern .||. miscPattern) $ version "raw" $ do + route $ setExtension "md" + compile $ getResourceBody >>= relativizeUrls + + create [ "archive.html" ] $ do + route idRoute + compile $ do + posts <- recentFirst =<< loadAll (postPattern .&&. hasNoVersion) + let archiveCtx = + listField "posts" postCtx (return posts) + <> constField "title" "Archives" + <> context + + makeItem "" + >>= loadAndApplyTemplate "templates/archive.html" archiveCtx + >>= loadAndApplyTemplate "templates/default.html" archiveCtx + >>= relativizeUrls + + match "index.html" $ do + route idRoute + compile $ do + allPosts <- recentFirst =<< loadAll (postPattern .&&. hasNoVersion) + let posts = take nbPostsHome allPosts + let indexCtx = + listField "posts" postCtx (return posts) + <> constField "title" "Accueil" + <> context + + getResourceBody + >>= applyAsTemplate indexCtx + >>= loadAndApplyTemplate "templates/default.html" indexCtx + >>= relativizeUrls + + match "templates/*.html" $ compile templateBodyCompiler + +-------------------------------------------------------------------------------- + +abstract :: Pandoc -> Maybe [Inline] +abstract (Pandoc _ blocks) = + markedUp blocks <|> fallback blocks + where + markedUp = fmap getFirst . query $ \inl -> case inl of + Span (_, cls, _) inls | "abstract" `elem` cls -> First (Just inls) + _ -> mempty + fallback (Para inlines : Header _ _ _ : _) = Just inlines + fallback (_ : t) = fallback t + fallback [] = Nothing + +customPandocCompiler :: Compiler (Item String) +customPandocCompiler = pandocCompilerWithTransformM + defaultHakyllReaderOptions + defaultHakyllWriterOptions { writerHTMLMathMethod = MathML } + (\pandoc -> do + let render = fmap writePandoc . makeItem . Pandoc mempty . pure . Plain + maybe + (pure ()) + (void . (saveSnapshot "abstract" <=< render)) + (abstract pandoc) + pure $ addSectionLinks pandoc + ) >>= relativizeUrls + +addSectionLinks :: Pandoc -> Pandoc +addSectionLinks = walk f where + f (Header n attr@(idAttr, _, _) inlines) = + let link = Link ("", ["section"], []) [Str "§"] ("#" <> idAttr, "") + in Header n attr ([link, Space] <> inlines) + f x = x + +snapshotField :: String -> Snapshot -> Context String +snapshotField key snap = field key $ \item -> + loadSnapshotBody (itemIdentifier item) snap + +context :: Context String +context = + mapContext escapeHtml metadataField + <> snapshotField "abstract" "abstract" + <> defaultContext + +postCtx :: Context String +postCtx = + dateField "date" "%Y-%m-%d" + <> context + +postCtxWith :: Tags -> Tags -> Tags -> Context String +postCtxWith tags authors poles + = tagsFieldWith getPoles simpleRenderLink (mconcat . intersperse ", ") "poles" poles + <> tagsFieldWith getAuthors simpleRenderLink (mconcat . (\l -> if length l == 0 then [] else H.toHtml ("par " :: String):l) . intersperse ", ") "author" authors + <> tagsFieldWith getAuthors licenseRenderLink (mconcat . intersperse ", " . (\l -> if length l == 0 then [] else l ++ [H.toHtml ("and" :: String)])) "authorLicense" authors + <> tagsField "tags" tags + <> postCtx + +tagCompiler :: String -> Compiler (Item String) +tagCompiler tags = + makeItem "" + >>= loadAndApplyTemplate "templates/tags.html" ctx + >>= loadAndApplyTemplate "templates/default.html" ctx + >>= relativizeUrls + where + prettyTags = replaceAll ", " (const "
  • ") tags + ctx = constField "title" "Toutes les catégories" <> constField "alltags" prettyTags <> context + +authorCompiler :: String -> Compiler (Item String) +authorCompiler authors = + makeItem "" + >>= loadAndApplyTemplate "templates/authors.html" ctx + >>= loadAndApplyTemplate "templates/default.html" ctx + >>= relativizeUrls + where + prettyAuthors = replaceAll ", " (const "
  • ") authors + ctx = constField "title" "Tout les (co-)auteurs" <> constField "allauthors" prettyAuthors <> context + +getAuthors :: MonadMetadata m => Identifier -> m [String] +getAuthors identifier = do + metadata <- getMetadata identifier + return $ maybe [] (map trim . splitAll ",") $ lookupString "author" metadata + +getPoles :: MonadMetadata m => Identifier -> m [String] +getPoles identifier = do + metadata <- getMetadata identifier + return $ maybe [] (map trim . splitAll ",") $ lookupString "poles" metadata + +extractTags :: Rules Tags +extractTags = do + tags <- buildTags postPattern $ fromCapture "tags/*.html" + return $ sortTagsBy caseInsensitiveTags tags + +extractAuthors :: Rules Tags +extractAuthors = do + authors <- buildTagsWith getAuthors postPattern $ fromCapture "authors/*.html" + return $ sortTagsBy caseInsensitiveTags authors + +extractPoles :: Rules Tags +extractPoles = do + poles <- buildTagsWith getPoles postPattern $ fromCapture "poles/*.html" + return $ sortTagsBy caseInsensitiveTags poles + +simpleRenderLink :: String -> (Maybe FilePath) -> Maybe H.Html +simpleRenderLink _ Nothing = Nothing +simpleRenderLink tag (Just filePath) = + Just $ H.a H.! A.href (H.toValue $ toUrl filePath) $ H.toHtml tag + +licenseRenderLink :: String -> (Maybe FilePath) -> Maybe H.Html +licenseRenderLink _ Nothing = Nothing +licenseRenderLink tag (Just filePath) = + Just $ H.a H.! A.href (H.toValue $ toUrl filePath) H.! A.rel "cc:attributionURL dct:creator" $ H.toHtml tag + +config :: Configuration +config = defaultConfiguration { destinationDirectory = "result" }