3 import Data.Default (def)
4 import Data.List qualified as List
5 import Data.Map.Strict qualified as Map
6 import Data.Set qualified as Set
7 import Data.Time qualified as Time
9 import Graphics.ThumbnailPlus qualified as Thumb
10 import Network.URI.Slug (Slug)
13 import Text.Blaze.Html5 ((!))
14 import Text.Blaze.Html5 qualified as H
15 import Text.Blaze.Html5.Attributes qualified as A
16 import Text.Blaze.Internal qualified as Blaze
17 import Text.Pandoc.Options qualified as Pandoc
18 import Text.Pandoc.Writers.Shared qualified as Pandoc
21 import Data.Text qualified as Text
25 import Text.Pandoc.Builder qualified as Pandoc
26 import Text.Pandoc.Shared qualified as Pandoc
27 import Text.Pandoc.Walk qualified as Pandoc
29 import Utils.Pandoc as Pandoc
30 import Utils.Pandoc.Html (htmlOfPandoc)
32 renderPage :: Model -> ([Slug], Page) -> Content
37 { pageMeta = meta@Meta{metaLang}
38 , pageDoc = Pandoc.Pandoc pandocMeta pandocBlocks
42 { contentTitle = Nothing -- Just metaTitle
44 H.div ! classes ["flex", "flex-row", "flex-wrap"] $ do
46 ! A.style "flex-grow: 2;"
66 -- H.hr ! classes ["min-w-full"]
74 -- ! A.style "width: 20em"
75 -- TODO: use column-gap: https://www.w3.org/TR/css-align-3/
76 -- once implemented in Web engines.
77 ! A.style "min-width:20rem; flex-grow: 1;"
95 , similarPages model (pagePath, pageMeta page)
99 H.aside ! classes ["mb-3"] $ aside
100 H.aside $ recentPages metaLang model (Just (metaTitle meta))
114 doc = Pandoc.makeSections True Nothing pandocBlocks
116 case Pandoc.toTableOfContents writerOpts $
117 Pandoc.walk walkToC doc of
118 Pandoc.BulletList [] -> Nothing
119 ul -> Just $ htmlOfPandoc writerOpts $ Pandoc.Pandoc pandocMeta [ul]
121 -- Hyper-link section numbers.
123 (divId, "section" : divCls, divDict)
124 (Pandoc.Header level ("", headerCls, headerDict) headerInlines : divBlocks) ->
126 (divId, "section" : divCls, divDict)
128 -- level + 1 to only have the title in a <h1>
131 (divId, headerCls, ("number", number) : headerDict)
132 (Pandoc.Space : Pandoc.Span ("", ["font-bold" | level == 1], []) headerInlines : []) :
135 sectionNum = fromMaybe mempty $ List.lookup "number" headerDict
136 sectionNums = Text.split ('.' ==) sectionNum
138 | List.length sectionNums == 1 = sectionNum <> "."
139 | otherwise = sectionNum
144 { Pandoc.writerExtensions = Pandoc.enableExtension Pandoc.Ext_smart Pandoc.pandocExtensions
145 , -- makeSections is called here and customized
146 Pandoc.writerNumberSections = False
147 , -- toTableOfContents is called here and customized
148 Pandoc.writerTableOfContents = False
149 , Pandoc.writerSectionDivs = True
150 , Pandoc.writerTOCDepth = maxBound
152 pageHtml = htmlOfPandoc writerOpts $
155 Pandoc.Link (linkId, linkCls, linkKVs) label (uri, title)
156 | not (Text.isInfixOf ":" uri) -> do
157 -- keep only local URIs
159 (linkId, ["text-red-800"] <> linkCls, linkKVs)
161 (uri <> ".html", title)
164 $ (`Pandoc.walk` Pandoc.Pandoc pandocMeta doc) $ \case
165 Pandoc.BulletList items
168 Pandoc.Plain (Pandoc.Str "pictures:" : Pandoc.Space : Pandoc.Str _ : _) : [] -> True
187 $ H.text (metaTitle meta)
189 unless (null (metaTags meta)) do
196 --, "max-w-screen-md"
198 $ forM_ (metaTags meta) $
199 renderTag model False
201 whenJust (metaSummary meta) $ \Markdown{..} -> do
224 H.div ! classes ["px-2"] $
231 $ htmlOfPandoc def markdownPandoc
233 -- ! classes [ "bg-gray-500"
240 -- ! A.style "font-size: 0.55rem"
241 -- $ [fmt|{signsCount pageDoc} signes|]
242 renderPageHeaders = Just $
249 $ forM_ (zip [0 :: Int ..] headers) $ \(headerRank, (headerName, headerValue)) -> do
252 [ if headerRank `mod` 2 == 0
263 , "whitespace-nowrap"
264 , if headerRank /= 0 then "border-t" else ""
267 H.text (headerName metaLang)
272 , if headerRank /= 0 then "border-t" else ""
277 dateHtml = Time.formatTime (timeLocale metaLang) "%-e %B %Y"
278 headers :: [(Lang -> Text, H.Html)]
281 [ metaAuthors meta >>= \authors ->
285 forM_ authors $ \Entity{..} ->
288 Nothing -> H.text entityName
291 ! A.href ("mailto:" <> H.textValue mail)
294 , metaUpdated meta >>= \date ->
295 Just (i18nUpdated, H.string $ dateHtml date)
296 , metaPublished meta >>= \date ->
297 Just (i18nPublished, H.string $ dateHtml date)
298 , let lisense = fromMaybe "CC-BY-SA-4.0" (metaLisense meta)
299 in Just (i18nLisense, H.a ! A.href [fmt|https://spdx.org/licenses/{lisense}.html|] $ H.text lisense)
300 , metaDiscussion meta >>= \Discussion{..} ->
303 , H.a ! A.href (H.textValue [fmt|mailto:{discussionMail}|]) $
304 H.text discussionMail
306 , Just (i18nLanguage, H.text $ langText metaLang)
309 tocHtmlMaybe <&> \tocHtml ->
315 H.details ! A.open "" $ do
331 $ i18nTableOfContent metaLang
340 renderPageBackLinks =
341 let bls = backLinks (LocalLink pagePath) modelLocalLinks
350 H.details ! A.open "" $ do
366 $ i18nBacklinks metaLang
368 forM_ (modelLocalLinks Map.! pagePath) $ \(LocalLink lnk) ->
369 H.li $ H.string $ show lnk
382 ! A.href (H.stringValue (Ema.encodeRoute model (Right @FilePath (RoutePage bl))))
383 $ H.text (metaTitle (pageMeta (modelPosts Map.! bl)))
384 renderPageDiscussion =
385 whenJust (metaDiscussion meta) $ \Discussion{..} -> do
410 ! A.href "#discussion"
411 ! classes ["text-white"]
412 $ H.text (i18nDiscussion metaLang)
414 ! classes ["font-normal", "text-sm"]
417 H.a ! A.href ("mailto:" <> H.textValue discussionMail) $
418 H.text discussionMail
424 , --, "float-left", "clear-left"
427 ! A.style "height:100vh"
428 ! A.src (H.textValue discussionUrl)
429 ! A.src "http://oignon.wg:8000"
430 ! Blaze.attribute "loading" " loading=\"" "lazy"
434 ("", ["pictures", "mb-2"], [])
435 [ Pandoc.BulletList $
438 Pandoc.Plain (Pandoc.Str "pictures:" : Pandoc.Space : Pandoc.Str (decodeSlugs -> prefix) : plainInlines) : [] ->
439 [ let alt = Pandoc.trimWhiteInlines plainInlines
440 in let (maxThumbSize, maxThumbPath) = List.last thumbs
441 in let (minThumbSize, _minThumbPath) = List.head $ traceShowId thumbs
447 Pandoc.RawInline "html5" [fmt|
448 <picture><source srcset="/{}" media="(min-width: {}px)"></picture>
456 , Text.intercalate ", " $
457 thumbs <&> \(size, name) ->
458 [fmt|/{name} {Thumb.width size}w|]
464 [ [fmt|(min-width: 640px) {Thumb.width maxThumbSize}px|]
465 , [fmt|{Thumb.width minThumbSize}px|]
471 ( toText $ '/' : maxThumbPath
472 , Pandoc.stringify alt -- title
475 (encodeSlugs slugs, "")
478 [ Pandoc.Str [fmt|{Thumb.width maxThumbSize}x{Thumb.height maxThumbSize}|]
481 | (slugs, thumbs) <- Map.toList modelPictures
482 , prefix `List.isPrefixOf` slugs
488 LangEn -> "Author(s)"
489 LangFr -> "Auteur.rice(s)"
490 _ -> i18nUpdated LangEn
491 i18nBacklinks = \case
492 LangEn -> "Backlinks"
493 LangFr -> "Rétroliens"
494 _ -> i18nBacklinks LangEn
498 _ -> i18nLanguage LangEn
502 _ -> i18nLisense LangEn
503 i18nDiscussion = \case
504 LangEn -> "Discussion"
505 LangFr -> "Discussion"
506 _ -> i18nDiscussion LangEn
509 LangFr -> "Mise-à-jour"
510 _ -> i18nUpdated LangEn
511 i18nPublished = \case
512 LangEn -> "Published"
513 LangFr -> "Publication"
514 _ -> i18nPublished LangEn
515 i18nLatestPosts = \case
516 LangEn -> "Latest Posts"
517 LangFr -> "Derniers Billets"
518 _ -> i18nLatestPosts LangEn
519 i18nTableOfContent = \case
520 LangEn -> "Table of Content"
522 _ -> i18nTableOfContent LangEn
524 similarPages :: Model -> ([Slug], Meta) -> Maybe H.Html
525 similarPages model (pagePath, meta) =
548 H.text "Similar Posts"
549 H.span ! classes ["font-normal"] $
550 H.text [fmt| ({List.length simPosts})|]
551 renderPagesListing model posts
554 List.filter (\(path, Page{..}) -> not (null (commonTags meta pageMeta)) && path /= pagePath) $
555 filterPosts model noFilter
556 posts = take 5 $ List.sortOn (negate . List.length . commonTags meta . pageMeta . snd) simPosts
557 commonTags x y = tagSet x `Set.intersection` tagSet y
559 tagSet = fromList . metaTags
561 renderPagesListing :: Model -> [([Slug], Page)] -> H.Html
562 renderPagesListing model pages =
564 forM_ pages $ \(pagePath, Page{..}) ->
573 H.h6 ! classes ["text-sm"] $ do
574 H.a ! classes ["hover:bg-blue-50", "rounded"]
575 ! hrefRoute model (RoutePage pagePath)
576 $ H.text $ metaTitle pageMeta
577 unless (null (metaTags pageMeta)) do
579 ! classes ["flex", "flex-row", "flex-wrap"]
580 $ forM_ (metaTags pageMeta) $
581 renderTag model False
582 whenJust (metaSummary pageMeta) $ \Markdown{..} -> do
584 ! classes ["italic", "text-gray-700"]
585 $ htmlOfPandoc def markdownPandoc
587 timeLocale :: Lang -> Time.TimeLocale
590 Time.defaultTimeLocale
593 , ("février", "Févr")
598 , ("juillet", "Juil")
600 , ("septembre", "Sep")
602 , ("novembre", "Nov")
603 , ("décembre", "Déc")
606 _ -> Time.defaultTimeLocale
608 renderSpecial :: Model -> Page -> Content
609 renderSpecial model Page{..} =
611 { contentTitle = Just $ metaTitle pageMeta
625 $ H.text $ metaTitle pageMeta
626 htmlOfPandoc def pageDoc
627 recentPages (metaLang pageMeta) model Nothing
630 recentPages :: Lang -> Model -> Maybe Text -> H.Html
631 recentPages lang model here =
650 $ i18nLatestPosts lang
651 renderPagesListing model posts
655 List.filter (\(_, Page{..}) -> isJust (metaUpdated pageMeta) && maybe True (/= metaTitle pageMeta) here) $
656 filterPosts model noFilter
658 filterPosts :: Model -> Filter -> [([Slug], Page)]
659 filterPosts Model{..} (Filter lang tag) =
661 List.sortOn (metaUpdated . pageMeta . snd) $
662 maybe id (\t -> List.filter (\(_, Page{..}) -> t `elem` metaTags pageMeta)) tag $
663 maybe id (\l -> List.filter (\(_, Page{..}) -> l == metaLang pageMeta)) lang $
664 Map.toList modelPosts