neuron-0.2.0.0: src/Neuron/Zettelkasten/View.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}
{-# LANGUAGE NoImplicitPrelude #-}
-- | HTML & CSS
module Neuron.Zettelkasten.View where
import Clay hiding (head, id, ms, reverse, s, type_)
import qualified Clay as C
import Data.Foldable (maximum)
import Data.Tree (Tree (..))
import Lucid
import Neuron.Version (neuronVersionFull)
import Neuron.Zettelkasten.Config
import Neuron.Zettelkasten.Graph
import Neuron.Zettelkasten.ID (ZettelID (..), zettelIDSourceFileName)
import Neuron.Zettelkasten.Link (linkActionExt)
import Neuron.Zettelkasten.Link.Action (LinkTheme (..))
import Neuron.Zettelkasten.Link.View (renderZettelLink)
import Neuron.Zettelkasten.Markdown (neuronMMarkExts)
import Neuron.Zettelkasten.Meta
import Neuron.Zettelkasten.Route
import Neuron.Zettelkasten.Store
import Neuron.Zettelkasten.Type
import Relude
import qualified Rib
import Rib.Extra.CSS (mozillaKbdStyle)
import qualified Rib.Parser.MMark as MMark
import Text.MMark (useExtensions)
import Text.Pandoc.Highlighting (styleToCss, tango)
renderRouteHead :: Monad m => Config -> Route store graph a -> store -> HtmlT m ()
renderRouteHead config r val = do
meta_ [httpEquiv_ "Content-Type", content_ "text/html; charset=utf-8"]
meta_ [name_ "viewport", content_ "width=device-width, initial-scale=1"]
title_ $ toHtml $ routeTitle config val r
link_ [rel_ "shortcut icon", href_ "https://raw.githubusercontent.com/srid/neuron/master/assets/logo.ico"]
case r of
Route_IndexRedirect ->
mempty
_ -> do
toHtml $ routeOpenGraph config val r
style_ [type_ "text/css"] $ styleToCss tango
renderRouteBody :: Monad m => Config -> Route store graph a -> (store, graph) -> HtmlT m ()
renderRouteBody config r val = do
case r of
Route_ZIndex ->
renderIndex val
Route_Zettel zid ->
renderZettel config val zid
Route_IndexRedirect ->
meta_ [httpEquiv_ "Refresh", content_ $ "0; url=" <> Rib.routeUrlRel Route_ZIndex]
renderIndex :: Monad m => (ZettelStore, ZettelGraph) -> HtmlT m ()
renderIndex (store, graph) = do
h1_ [class_ "header"] $ "Zettel Index"
div_ [class_ "z-index"] $ do
-- Cycle detection.
case topSort graph of
Left (toList -> cyc) -> div_ [class_ "ui orange segment"] $ do
h2_ "Cycle detected"
forM_ cyc $ \zid ->
li_ $ renderZettelLink LinkTheme_Default store zid
_ -> mempty
let clusters = sortMothers $ zettelClusters graph
p_ $ do
"There " <> countNounBe "cluster" "clusters" (length clusters) <> " in the Zettelkasten graph. "
"Each cluster is rendered as a forest, with their roots (mother zettels) highlighted."
forM_ clusters $ \zids ->
div_ [class_ "ui piled segment"] $ do
let forest = dfsForestFrom zids graph
-- Forest of zettels, beginning with mother vertices.
ul_ $ renderForest True Nothing LinkTheme_Default store graph forest
where
-- Sort clusters with newer mother zettels appearing first.
sortMothers ms = reverse $ sortOn maximum $ fmap (reverse . sort . toList) ms
countNounBe noun nounPlural = \case
1 -> "is 1 " <> noun
n -> "are " <> show n <> " " <> nounPlural
renderZettel :: forall m. Monad m => Config -> (ZettelStore, ZettelGraph) -> ZettelID -> HtmlT m ()
renderZettel config@Config {..} (store, graph) zid = do
let Zettel {..} = lookupStore zid store
zettelTags = getMeta zettelContent >>= tags
div_ [class_ "zettel-view"] $ do
div_ [class_ "ui raised segment"] $ do
h1_ [class_ "header"] $ toHtml zettelTitle
renderTags `mapM_` zettelTags
let mmarkExts = neuronMMarkExts config
MMark.render $ useExtensions (linkActionExt store : mmarkExts) zettelContent
div_ [class_ "ui inverted teal top attached connections segment"] $ do
div_ [class_ "ui two column grid"] $ do
div_ [class_ "column"] $ do
div_ [class_ "ui header"] "Connections"
let forest = obviateRootUnlessForest zid $ dfsForestFrom [zid] graph
ul_ $ renderForest True (Just 2) LinkTheme_Simple store graph forest
div_ [class_ "column"] $ do
div_ [class_ "ui header"] "Navigate up"
let forestB = obviateRootUnlessForest zid $ dfsForestBackwards zid graph
ul_ $ do
renderForest True Nothing LinkTheme_Simple store graph forestB
div_ [class_ "ui inverted black bottom attached footer segment"] $ do
div_ [class_ "ui three column grid"] $ do
div_ [class_ "center aligned column"] $ do
a_ [href_ ".", title_ "/"] $ fa "fas fa-home"
div_ [class_ "center aligned column"] $ do
whenJust editUrl $ \urlPrefix ->
a_ [href_ $ urlPrefix <> zettelIDSourceFileName zid, title_ "Edit this Zettel"] $ fa "fas fa-edit"
div_ [class_ "center aligned column"] $ do
a_ [href_ (Rib.routeUrlRel Route_ZIndex), title_ "All Zettels (z-index)"] $
fa "fas fa-tree"
div_ [class_ "ui one column grid footer-version"] $ do
div_ [class_ "center aligned column"] $ do
p_ $ do
"Generated by "
a_ [href_ "https://github.com/srid/neuron"] "Neuron"
" "
code_ $ toHtml @Text neuronVersionFull
renderTags :: Monad m => [Text] -> HtmlT m ()
renderTags tags = do
div_ [class_ "ui tiny labels"] $ do
forM_ tags $ \tag -> do
div_ [class_ "ui lightgrey label"] $ toHtml @Text tag
div_ [class_ "ui divider"] $ mempty
-- | Font awesome element
fa :: Monad m => Text -> HtmlT m ()
fa k = with i_ [class_ k] mempty
renderForest ::
Monad m =>
Bool ->
Maybe Int ->
LinkTheme ->
ZettelStore ->
ZettelGraph ->
[Tree ZettelID] ->
HtmlT m ()
renderForest isRoot maxLevel ltheme s g trees =
case maxLevel of
Just 0 -> mempty
_ -> do
forM_ (sortForest trees) $ \(Node zid subtrees) ->
li_ $ do
let zettelDiv =
div_
[class_ $ bool "" "ui black label" $ ltheme == LinkTheme_Default]
bool id zettelDiv isRoot $
renderZettelLink ltheme s zid
when (ltheme == LinkTheme_Default) $ do
" "
case backlinks zid g of
conns@(_ : _ : _) ->
-- Has two or more backlinks
forM_ conns $ \zid2 -> do
let z2 = lookupStore zid2 s
i_ [class_ "fas fa-link", title_ $ unZettelID zid2 <> " " <> zettelTitle z2] mempty
_ -> mempty
when (length subtrees > 0) $ do
ul_ $ renderForest False ((\n -> n - 1) <$> maxLevel) ltheme s g subtrees
where
-- Sort trees so that trees containing the most recent zettel (by ID) come first.
sortForest = reverse . sortOn maximum
style :: Css
style = do
let linkColor = C.mediumaquamarine
linkTitleColor = C.auto
"span.zettel-link span.zettel-link-idlink a" ? do
C.fontFamily [] [C.monospace]
C.fontWeight C.bold
C.color linkColor
C.textDecoration C.none
"span.zettel-link span.zettel-link-idlink a:hover" ? do
C.backgroundColor linkColor
C.color C.white
".zettel-link .zettel-link-title" ? do
C.paddingLeft $ em 0.3
C.fontWeight C.bold
C.color linkTitleColor
"div.z-index" ? do
C.ul ? do
C.listStyleType C.square
C.paddingLeft $ em 1.5
"div.zettel-view" ? do
C.ul ? do
C.paddingLeft $ em 1.5
C.listStyleType C.square
C.li ? do
mempty -- C.paddingBottom $ em 1
C.h1 ? do
C.paddingTop $ em 0.2
C.paddingBottom $ em 0.2
C.textAlign C.center
C.color C.midnightblue
C.fontWeight C.bold
C.backgroundColor C.whitesmoke
C.h2 ? do
C.fontColor C.darkslategray
C.fontWeight C.bold
C.borderBottom C.solid (px 1) C.steelblue
C.marginBottom $ em 0.5
C.h3 ? do
C.fontColor C.slategray
C.fontWeight C.bold
C.margin (px 0) (px 0) (em 0.4) (px 0)
codeStyle
blockquoteStyle
kbd ? mozillaKbdStyle
"div.connections" ? do
"a" ? do
C.important $ color white
"a:hover" ? do
C.opacity 0.5
".footer" ? do
"a" ? do
C.color white
".footer-version, .footer-version a, .footer-version a:visited" ? do
C.color gray
".footer-version a" ? do
C.fontWeight C.bold
".footer-version" ? do
C.fontSize $ em 0.7
where
codeStyle = do
C.code ? do
sym margin auto
fontSize $ pct 90
"code, pre, tt" ? do
fontFamily ["SFMono-Regular", "Menlo", "Monaco", "Consolas", "Liberation Mono", "Courier New"] [monospace]
pre ? do
sym padding $ em 0.5
C.overflow auto
C.maxWidth $ pct 100
"div.source-code" ? do
marginLeft auto
marginRight auto
maxWidth $ pct 80
pre ? do
backgroundColor "#f8f8f8"
-- https://css-tricks.com/snippets/css/simple-and-nice-blockquote-styling/
blockquoteStyle = do
C.blockquote ? do
-- TODO: quotes in clay?
C.backgroundColor "#f9f9f9"
C.borderLeft C.solid (px 10) "#ccc"
sym2 C.margin (em 1.5) (px 10)
sym2 C.padding (em 0.5) (px 10)
C.p ? do
C.display C.inline
"blockquote:before" ? do
C.color "#ccc"
C.content C.openQuote
C.fontSize $ em 4
C.lineHeight $ em 0.1
C.marginRight $ em 0.25
C.verticalAlign $ em $ -0.4